상세 컨텐츠

본문 제목

C# 사용자 지정 특성 작성

프로그래밍/C#

by TickTack 2021. 9. 27. 15:43

본문

이번에는 사용자 지정 특성을 만드는 법에 대하여 알아보겠습니다.

테스트 환경은 (Visual Studio 2010, .Net 4.0)에서 진행하였습니다.

 

AttributeUsage를 사용하여 작성하는데 해당 클래스를 보면 System.AttributeUsageAttribute 네임스페이스안에

속해있지만 using System; 만 선언해도 오류가 나지 않습니다.

먼저 해당 클래스의 형태를 보겠습니다.

[AttributeUsage(AttributeTargets.All)]
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]

[]안에 선언을 하는 형태로 되어있고, 속성은 최대 3개가 포함되어 있습니다.

또한 AttributeTargets만 선언도 가능하도록 오버로드 되어있고,

선언하지 않을경우 Inherited의 기본값은 true, AllowMultiple의 기본값은 false입니다.

그리고 MSDN에서 해당 형태로 선언한 클래스는 끝에 Attribute를 붙이는것이 가독성을 위해서 좋다고 합니다.

사용할 때는 Attribute를 생략 가능하다고도 하고요.

또한 생성할 특성 클래스에 Attribute를 상속시키지 않으면 [ATest1Attribute]를 선언할 수 없습니다.

이제 각 속성에 대해서 하나씩 들여다보겠습니다.

 

- AttributeTargets

해당 속성은 열거형태로 되어있고, 여러 값들로 구성되어 있습니다.

또한 선언시에 OR 연산자인 | 를 이용하여 여러가지를 선언할 수 있습니다.

그 중에서 일부를 보면

 

- All : 모든 종류에 선언이 가능합니다.

- Class : 클래스에만 선언이 가능합니다.

- Method : 메서드에만 선언이 가능합니다.

- Event : 이벤트에만 선언이 가능합니다.

- Interface : 인터페이스에만 선언이 가능합니다.

 

의 의미를 가집니다.

만약 아래와 같이 하게되면 빌드를 하기 전 까지는 문제가 없지만

public partial class Test2 : Form
{
    [ATest1Attribute]
    public Test2()
    {
        InitializeComponent();
        short i = 123;
        object o = i;
        int j = (short)o;
    }
}

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public class ATest1Attribute : Attribute
{
    public ATest1Attribute()
    {
        Console.WriteLine("Test1");
    }
}

빌드를 하게되면 다음과 같이 오류가 발생합니다.

사진속의 ATest2Attribute는 신경쓰지 않으셔도 됩니다.

따라서 다음과 같이 수정해주어야 합니다.

[ATest1Attribute]
public partial class Test2 : Form
{
    public Test2()
    {
        InitializeComponent();
        short i = 123;
        object o = i;
        int j = (short)o;
    }
}

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public class ATest1Attribute : Attribute
{
    public ATest1Attribute()
    {
        Console.WriteLine("Test1");
    }
}

그러나 위와 같은 코드로는 실행 시 반영이 제대로 된건지 눈으로 보이지 않습니다.

따라서 콘솔창에 텍스트를 출력하기 위해 Test2 클래스의 생성자에 다음과 같이 추가해줍니다.

위에 특성 선언시에 [ATest1]으로 되어있는 이유는 ATest1Attribute의 'Attribute'가 생략 가능하기 때문입니다.

[ATest1]
public partial class Test2 : Form
{
    public Test2()
    {
        InitializeComponent();
        short i = 123;
        object o = i;
        int j = (short)o;

        ATest1Attribute aTest1 = new ATest1Attribute();
    }
}

 

- Inherited

해당 특성을 사용한 클래스를 상속하였을때 특성도 같이 상속할 수 있는지 여부를 나타냅니다.

다음은 차이점을 확인할 수 있는 예시 코드입니다.

public class Program
{
    public static void Main(string[] args)
    {
        Type typeA = typeof(DerivedA);
        MethodInfo memberA = typeA.GetMethod("MethodA");
        Console.WriteLine("MethodA 길이 : " + memberA.GetCustomAttributes(typeof(InheritedAttribute), true).Length);
        Console.WriteLine(memberA.GetCustomAttributes(typeof(InheritedAttribute), true)[0]);

        Type typeB = typeof(DerivedB);
        MethodInfo memberB = typeB.GetMethod("MethodB");
        Console.WriteLine("MethodB 길이 : " + memberB.GetCustomAttributes(typeof(NotInheritedAttribute), true).Length);
        Console.WriteLine(memberB.GetCustomAttributes(typeof(NotInheritedAttribute), true)[0]);
	}
}

[InheritedAttribute]
public class BaseA
{
    [InheritedAttribute]
    public virtual void MethodA()
    { }
}

public class DerivedA : BaseA
{
    public override void MethodA()
    { }
}

[NotInheritedAttribute]
public class BaseB
{
    [NotInheritedAttribute]
    public virtual void MethodB()
    { }
}

public class DerivedB : BaseB
{
    public override void MethodB()
    { }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
public class InheritedAttribute : Attribute
{ }

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
public class NotInheritedAttribute : Attribute
{ }

Inherited가 true로 된 InheritedAttribute 특성과

Inherited가 false로 된 NotInheritedAttribute 특성이 있습니다.

이것을 각각 별도의 클래스에 적용시킨 후 가상 메서드에도 적용시킵니다.

그리고 클래스를 추가로 만든 다음 특성을 적용시킨 클래스를 상속시킨 후 가상 메서드를 재정의합니다.

 

그 다음 Main 함수에서 상속받은 클래스의 타입을 구하고 GetMethod 함수로 메서드 정보를 가져옵니다.

그리고 해당 인스턴스에 GetCustomAttributes 함수를 이용하여 사용자 지정 특성의 배열을 가져옵니다.

다음은 결과창입니다.

Inherited가 false로 되어있는 특성이 적용된 MethodB는 배열에 정보가 하나도 없는 것을 알 수 있습니다.

 

- AllowMultiple

하나의 요소에 대해 여러개의 특성 인스턴스를 사용할 수 있는지에 대한 값 입니다.

다음은 예시 코드입니다.

public class Program
{
    public static void Main(string[] args)
    {
        
    }
}

[AllowMultipleAttribute]
[AllowMultipleAttribute]
public class BaseA
{
    public virtual void MethodA()
    { }
}

public class DerivedA : BaseA
{
    public override void MethodA()
    { }
}

[NotAllowMultipleAttribute]
[NotAllowMultipleAttribute]
public class BaseB
{
    public virtual void MethodB()
    { }
}

public class DerivedB : BaseB
{
    public override void MethodB()
    { }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class AllowMultipleAttribute : Attribute
{ }

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public class NotAllowMultipleAttribute : Attribute
{ }

이렇게 하면 빌드했을 때 특성이 중복되었다는 오류가 뜨면서 컴파일이 되지 않는 것을 볼 수 있습니다.

원래 예시 코드대로만 선언해서 사용한다면 저렇게 선언을 중복으로 할 이유가 딱히 없습니다.

그러나 예시코드와는 달리 실제로 사용할 때는 단순히 저렇게 선언만 하는 것이 아니라

다음과 같은 형태로 사용할 수도 있기 때문에 여러 개를 같은 곳에 선언하는 경우도 있다는 것입니다.

[AllowMultiple("Name", "Age", Reviewed = true)]

단, 위와 같은 형태처럼 사용하기 위해서는 특성 클래스 내부에 속성을 만들어주어야 합니다.

 

- 사용 예시

이제 실제로 활용을 해보도록 하겠습니다.

소스는 아래와 같습니다.

[Test("TestName", "TestLevel", Reviewed = true, Age = 17)]
public class Program
{
    public static void Main(string[] args)
    {
        GetAttribute(typeof(Program));
    }

    public static void GetAttribute(Type t)
    {
        TestAttribute MyAttribute = (TestAttribute)Attribute.GetCustomAttribute(t, typeof(TestAttribute));

        if (MyAttribute == null)
        {
            Console.WriteLine("The attribute was not found.");
        }
        else
        {
            Console.WriteLine("The Name Attribute is: {0}.", MyAttribute.Name);
            Console.WriteLine("The Level Attribute is: {0}.", MyAttribute.Level);
            Console.WriteLine("The Reviewed Attribute is: {0}.", MyAttribute.Reviewed);
            Console.WriteLine("The Age Attribute is: {0}.", MyAttribute.Age);
        }
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class TestAttribute : Attribute
{
    private string name;
    private string level;
    private bool reviewed;
    private int age;

    public TestAttribute(string name, string level)
    {
        this.name = name;
        this.level = level;
        this.reviewed = false;
        this.age = 0;
    }

    public virtual string Name
    {
        get { return name; }
    }

    public virtual string Level
    {
        get { return level; }
    }

    public virtual bool Reviewed
    {
        get { return reviewed; }
        set { reviewed = value; }
    }

    public virtual int Age
    {
        get { return age; }
        set { age = value; }
    }
}

생성자에 포함된 속성만 필수 항목이며, 나머지는 넣지 않으면 기본으로 설정된 값이 나오게 됩니다.

그리고 속성에 선언된 virtual은 없어도 동작에는 문제없으니 필요없다면 빼도 무방합니다.

또한 속성의 형태를 더 간단하게 하고 싶고 get, set 둘 다 넣어도 상관없다고 하면 아래와 같이해도 됩니다.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public class TestAttribute : Attribute
{
    public TestAttribute(string name, string level)
    {
        Name = name;
        Level = level;
        Reviewed = false;
        Age = 0;
    }

    public string Name { get; set; }
    public string Level { get; set; }
    public bool Reviewed { get; set; }
    public int Age { get; set; }
}

 

이상으로 사용자 정의 특성에 대하여 알아보았습니다.

사실 이렇게 복잡하게 쓸 일이 있을까 싶긴 하지만 혹시 모르는 일이고 알아두어서 나쁠 건 없다고 봅니다.

관련글 더보기

댓글 영역