본문 바로가기

.NET Framework/C# Common

[어트리뷰트] 02 Custums Attributes 정의

 회차
 
01. Attributes - Overview of Attributes
 02. Attributes - Custom Attributes

02 | 사용자 정의형(Custom Attributes)
지난 작성된 글로부터 어트리뷰트란 무엇인지에 대해 알아보고 닷넷 프레임워크에서 미리 정의되어 있는 어트리뷰트에 대해서 살펴 보았습니다. 사용자 정의형 어트리뷰트에 대해서 정의와 사용법에 대해서 알아보도록 하겠습니다.

* Attributes Scope 정의
지난 아티클로 통해 Conditional Attribute에 대한 적용되는 요소에 대해서 설명을 하였습니다. 그렇다면 사용자가 직접 정의한 어트리뷰트 또한 적용 되지 않을까요? AttributeUsage를 이용한다면 작성한 어트리뷰트가 어떤 데이터형, 어떤 곳에 적용 시킬수 있을지 구체적으로 명시를 할 수 있습니다.

[System.AttributeUsage(AttributeTargets.Class)]
public class MyAttribute : Attribute
{
    //TODO
}

위 코드를 보시면 어트리뷰트를 클래스에 적용 하듯이 똑 같은 형태입니다.

이제부터 작성한 어트리뷰트는 적용시 AttributeUsage 어트리뷰트를 접미어를 선언하여 이용하시면 됩니다. 여기서 AttributeTargets.Class는 위치지정 파라미터가 됩니다. 만약 클래스가 아닌 다른 타입에 적용시 컴파일시 에러가 발생되니 유념하시길 바랍니다.
기본적으로 어트리뷰트는 System.Attribute클래스로부터 상속을 받아 정의합니다. 어트리뷰트도 하나의 클래스이므로 생성자나 멤버변수(필드), 메소드등을 가질수 있으며 상속도 가능합니다. 어트리뷰트 또한 일반적인 클래스처럼 생성자가 명시적으로 써주지 않아도 컴파일러에 의해 묵시적으로 디폴트 생성자를 가질 수 있습니다.

이 어트리뷰트의 용도는 해당 어트리뷰트를 사용할 수 있는 타겟(아래 표 참조)을 지정하고 어트리뷰트의 중복을 허용하거나 어트리뷰트에 대한 상속 허용 등을 지정할 때 사용합니다.

다음 System.AttributeTargets에 정의되어 있는 AttributeUsage안에 정의되어있는 파라미터들은 열거형 변수로 다음과 같습니다.

Member 어트리뷰트가 타겟 되어지는 곳
Class Class
Construct constructor
Delegate delegate
Enum enum
Event event
Field field
Sturct return value
Assembly struct
Property property
Parameter parameter
Method method
Interface interface
Module module
ClassMembers class, struct, enum, method, field, delegate, interface, constructor, property, event
All 모든 곳

* Attributes Class 정의(선언)
예)
[System.AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=true)]
class MyAttribute : Attribute {
    private String firstName;
    private String lastName;

    public MyAttribute(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

class My2Attribute : MyAttribute {
   private String middleName;
   public String MiddleName {
       get { return middleName; }
       set { middleName = value; }
   }
public My2Attribute() : base("defaultFirstName","defaultLastName") {}
}

위 예제에서는 어트리뷰트를 클래스와 메소드 레벨에서 사용할 수 있도록 지정하였으며 중복을 허용합니다. 그리고 상속을 지정하지 않는 경우 true 값이 설정됩니다.
위 예제처럼 여러 개의 데이터형에 라벨을 지정 하려면 ‘ | ‘ 파이프 연산자로 구분해주면 됩니다.
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)]

모든 데이터형에 라벨을 붙이고 싶다면 AttributeTarget.All을 사용해주시면 됩니다.
[AttributeUsage(AttributeTargets.All)]

어트리뷰트 클래스 선언 방법은 보통 클래스 선언하는 것과 동일한 방법으로 선언하고 다만 System.Attribute로 부터 상속을 받으면 그 클래스가 어트리뷰트가 됩니다.
일반 클래스오 구별하귀 위해서 클래스 이름 접미사로 ‘Attribute’를 붙이는 것을 권장합니다. 부가적인 정보를 기록한다는 의미로 다음 예제와 같이 클래스 이름을 ‘MyInfoAttribute’ 어트리뷰트를 선언하여 사용 하겠습니다.

* 어트리뷰트 클래스 선언

[AttributeUsage(AttributeTargets.Class)]
public class MyInfoAttribute : Attribute
{
       private string _korName;
       private string _engName;
       private string _birthday;

       public MyInfoAttribute(string korName, string engName, string birthday)
       {
             this._korName = korName;
             this._engName = engName;
             this._birthday = birthday;
       }
       public string KorName 
       {
             get { return this._korName; }
       }
       public string EngName
       {
             get { return this._engName; }
             set { this._engName = value; }
       }
       public string Birthday 
       {
             get { return this._birthday; }
       } 


* 어트리뷰트 사용
[MyInfo("정은성", EngName = "Jeong Eun Seong", Birthday = "1984-12-02")]    class People { }

* 어트리뷰트 정보 얻기
class Test
{
       static void Main(string[] args) 
       {
               Type type = typeof(People);
                foreach (Attribute attri in type.GetCustomAttributes(true))
                {
                         MyInfoAttribute myInfo = attri as MyInfoAttribute;
                         if (null != myInfo)
                         {
                                 Console.WriteLine("한국 이름은 {0}, \n" +
                                                            "영문 이름은 {1}, \n" +
                                                            "생년 월일은 {2}",
                                  myInfo.KorName, myInfo.EngName, myInfo.Birthday);
                         }
                }
        }
}
* 결과
한국 이름은 정은성,
영문 이름은 Jeong Eun Seong,
생년 월일은 1984-12-02


위 예제 소스를 통해 어트리뷰트 클래스 선언과 사용에 대해 보셨습니다

코드를 통해 어트리뷰트 전체 이름(MyInfoAttribute)을 사용하셔도 되며 어트리뷰트 명에서 Attribute 접미사를 제외한 이름(MyInfo)만 사용해도 됩니다.여기서
주의사항은 어트리뷰트는 클래스 이므로 생성자를 가질수 있지만 반드시 생성자는 하나이어야만 합니다. 즉, 생성자를 오버로딩을 할 수 없습니다.

만약 추가적인 데이터를 집어 넣고 싶거나 변경하고 싶다면 일반 클래스에서는 생성자를 오버로딩해서 생성자를 하나 만들어 사용할 수 있었지만 어트리뷰트 클래스에서는 생성자를 꼭 하나의 가져야 하기 때문에 동일한 방법으로 할 수가 없습니다. 하지만 꼭 넣어야 할 데이터가 있다면 위치 지정 파리미터를 이용하여 추가적 데이터의 성격을 갖는 것을 명명 파리미터로 받아 이런 문제점을 해결합니다.

위 코드 중 class People { } 위에 다음과 같은 어트리뷰트를 보시면


([MyInfo("정은성", EngName = "Jeong Eun Seong", Birthday = "1984-12-02")])


클래스에 어트리뷰트를 타겟을 한 모습을 볼 수 있습니다.

근데 무언가 다르게 보이지 않나요?어떤 것은 인수만, 어떤것은 LValue에 RValue를 할당의 형태를 볼수 있습니다. 이들의 차이점를 분류 한다면 위치지정 파라미터와 명명 파리미터로 분류 할수 있습니다.

위치지정 파리미터는 생성자에 값을 그대로 전달하면 되지만 명명 파라미터는 파라미터 이름에 값을 대입해서 생성자에 넘겨야 합니다 
Tip>사용자가 정보(ex>영어표기법이 변동이 생길 경우)를 변경/추가이 있을경우 명명 파라미터를 이용

이렇게 선언 위치는 일반적으로 대부분의 어트리뷰트는 타깃 타입의 바로 위에 선언하며 어트리뷰트 식별 (Attribute Identifier)를 이용하여 어트리뷰트의 적용 대상을 명확히 할 수 있습니다.그 외 러턴값에 어트리뷰트를 적용하고 싶다면 메소드 위에 선언할 때에 [retrun:어트리뷰트]와 같이 선언하면 되며 어셈블리에 대한 어트리뷰트의 경우 AssemblyInfo파일에 [assembly:MyAttribute(“정은성”,”JeongEunSeong”)]와같이 선언 하면 됩니다.

People 클래스에 라벨을 붙여 타겟(사용)을 하였다면 People클래스로부터 어트리뷰트 정보를 가져오는 예제에 대해서 앞서 살펴 보셨습니다. 그렇다면 어떤 원리에 의해서 가져왔을까요? 살펴보도록 하겠습니다.


Type type = typeof(People);

어트리뷰트 정보를 얻기위해서 우선 People 클래스의 특성을 알아내기 위해서 typeof 연산자를 이용하여 Type 객체를 생성합니다.

type.GetCustomAttributes(true);

Tip> Type 클래스(System.Type)에 보시면 GetCustomAttributes(bool inherit) 메소드가 존재합니다. 이 메소드는 파생된 클래스로부터 멤버의 상속 체인 여부나 재정의되는 경우 특성을 포함하는 배열을 반환합니다. 즉, 클래스에 부착된 모든 어트리뷰트를 배열로 반환 합니다.

Attributes 컬렉션들은 배열로 반환하므로 반복 제어문에 의해서  배열의 개수 만큼 벨리데이션 체크를 하여 타겟 어트리뷰트가 존재하는지 비교검사를 해야합니다.

foreach (Attribute attri in type.GetCustomAttributes(true)) {
   //TODO :
}


Attributes 컬렉션을 Attribute 타입으로 담아왔지만 어떤 어트리뷰트들이 넘어올지 모릅니다.위  예제에서는 단 하나의 어트리뷰트에 대한 정보를 기술 했지만 2개 이상의 어트리뷰트를 정의 했더라면 해당 어트리뷰트에 대한 다운 캐스팅을 하여 처리해야 합니다.
Tip> 각각에 대한 캐스팅을 올바르지 하지 않는 다면 예외처리가 발생할수 있으므로 주의 하세요 ^^;

MyInfoAttribute myInfo = attri as MyInfoAttribute

위 코드와 as 연산자를 사용하여 해당 어트리뷰트가 아닐경우 null 처리하고 null 이 아니라면 원하는 필드의 특성에 대한 정보를 조회 하실수 있습니다.^^

* 사용자 정의 어트리뷰트의 처리과정
컴파일러가 구문 실행중 어트리뷰트를 파싱하기 까지 이것을 어떻게 처리 해야할지? 궁금하지 않으신가요?
사용자 정의 어트리뷰트의 처리 과정은 다음과 같은 과정을 거쳐서 결정하게 됩니다.


사용자 정의 어트리뷰트의 처리과정
  1. 어트리뷰트 클래스 찾는다.
  2. 어트리뷰트의 범위를 체크한다.
  3. 어트리뷰트의 생성자를 체크 한다.
  4. 객체의 인스턴스를 생성한다.
  5. 명명 파라미터들을 체크한다.
  6. 명명 파리미터 값으로 필드나 프로퍼티 값을 설정한다.
  7. 어트리뷰트 클래스의 현재 상태를 저장한다.

다음 코드를보며 처리과정에 숨겨진 사실?! 알려드리겠습니다.
[AttributeUsage(AttributeTargets.Class)]
public class MyInfoAttribute : Attribute
{
    //TODO :
}
[MyInfo("정은성", EngName = "Jeong Eun Seong", Birthday = "1984-12-02")]
class People { }

코드를 보시면 Pepele 클래스위에 존재하는 MyInfo 과 어트리뷰트 정의한 이름인 MyInfoAttribute이 있습니다. 2개의 이름이 다르며 왜 달라?! 라고 의심스러울수도 있습니다. 이는 컴파일러하고 실행 하여도 아무런 문제가 없습니다. 왜 그럴까요?

OOP기법으로 설계를 하면서 Attribute를 생략하여도 아무런 문제 없도록 고안하였습니다

구체적으로 살펴본다면 컴파일시 해당 어트리뷰트를 찾게 되고 존재하지 않는다면 Attribute접미사를 붙여서 해당 어트리뷰트를 찾게됩니다. 마치 생성자를 생성할 때 묵시적으로 생성하는 개념하고 비슷하지 않나요 ^^? 이해가 되셨으면 합니다.

즉, People 클래스가 컴파일 될 때 컴파일러는 MyInfo 클래스를 찾게 되고 만약 존재하지 않는다면 컴파일러는 MyInfoAttribute 클래스를 찾게 됩니다. 그 이후 어트리뷰트가 클래스에 타겟되는 것을 허용하는지 체크를 합니다.

체크가 끝나면 생성자에게 넘겨진 파리미터와 어트리뷰트에 정의한 파라미터들이 일치하는지 검사하고 생성자에 넘겨진 값들을 가지고 객체의 인스턴스를 생성합니다. 만일 파라미터중에 명명 파라미터가 있다면 컴파일러는 어트리뷰트에서 그 파라미터와 매치되는 필드나 프로퍼티를 설정해줍니다. 그후 어트리뷰트 클래스의 현재 상태는 프로그램 요소들에 적용되는 메타 데이터로 저장이 됩니다.


* Mutilple Attribute의 사용
타겟이 되는 요소에 한개이상의 어트리뷰트 라벨을 부여 할수 있습니다.
public class CS {  
      ...(생략)   
      [conditional(“CLR”)]   
      [LOVE(“I Love C sharp”)]   
      public void method() {  }
}

하지만 제가 예제로 보여드렸던 코드에서 다음과 같이 기술한다면 에러가 발생합니다.
[MyInfo("정은성", "Jeong Eun Seong","")]
[MyInfo("데브피아", "Devpia","")]
[MyInfo("마이크로소프트", "Microsoft","")]
class People { }

왜냐하면 AllowMultiple = true로 설정을 해두지 못하여 발생되었습니다

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class MyInfoAttribute : Attribute
{
     //TODO :
}

포스팅을 마치며...

이번 포스팅은 너무나 늦게 올려드린것 같아요. 새로운 블로그로 이사 하는 과정에서 문제가 발생하여 아직 공개를 할수가 없었습니다. ^^ 용서 해주실꺼죠 ^^?
항상 좋은 하루 되시길 기원합니다.
정은성 드림

'.NET Framework > C# Common' 카테고리의 다른 글

[어트리뷰트] 01 개요(Overview of Attributes)  (7) 2010.05.29