[C#] 53. Reflection 기능을 사용하는 방법 - Attribute


Study/C#  2021. 10. 20. 18:52

안녕하세요. 명월입니다.


이 글은 C#에서 Reflection 기능을 사용하는 방법 - Attribute에 대한 글입니다.


지금까지 Reflection 기능을 소개했는데, 간단하게 요약하면 소스에 클래스 할당이나 함수 호출하는 정적인 방법을 데이터등에 의해 동적으로 클래스가 할당되거나 호출을 호출하는 방법입니다.

사실 C#에는 dynamic 자료형이 있고, 여러가지 패턴에 의한 프레임워크를 만들거나 코어 클래스를 만드는 것이 아니면 사실 사용할 일은 많이 있지 않습니다.


그런데 Reflection 기능 중에서 Attribute에 의한 클래스 구분이나 메소드 함수 호출 방식은 정말 많이 사용합니다.

link - [C#] 31. 어트리뷰트(Attribute)를 사용하는 방법


C#의 어트리뷰트(Attribute)는 메타 데이터로서의 역할 뿐인데, Reflection의 기능과 같이 사용되면 단순 메타 데이터의 기능이 아닌 프로그램을 제어하는 기능으로 사용할 수 있습니다.

using System;

// Class 타입의 어트리뷰트 생성
[AttributeUsage(AttributeTargets.Class)]
class DefaultData : Attribute
{
  // 어트리뷰트 필드 데이터
  public String Name { get; set; }
  // 생성자
  public DefaultData(String name)
  {
    this.Name = name;
  }
}

// Node1 클래스에 DefaultData 어트리뷰트 설정
[DefaultData("Process1")]
class Node1
{

}

// Node2 클래스에 DefaultData 어트리뷰트 설정
[DefaultData("Process2")]
class Node2
{

}
class Program
{
  static void Main(string[] args)
  {
    // Node1 클래스의 DefaultData 어트리뷰트 취득
    foreach (var attr in typeof(Node1).GetCustomAttributes(typeof(DefaultData), false) as DefaultData[])
    {
      // 콘솔 출력
      Console.WriteLine(attr.Name);
    }
    // Node2 클래스의 DefaultData 어트리뷰트 취득
    foreach (var attr in typeof(Node2).GetCustomAttributes(typeof(DefaultData), false) as DefaultData[])
    {
      // 콘솔 출력
      Console.WriteLine(attr.Name);
    }

    // 아무 키나 누르시면 종료합니다.
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 예제는 간단하게 Node1 클래스와 Node2 클래스의 어트리뷰트를 취득하는 예제입니다. 다른 Reflection 예제와 별반 차이가 없어보입니다.

using System;
using System.Linq;

// Class 타입의 어트리뷰트 생성
[AttributeUsage(AttributeTargets.Class)]
class DefaultData : Attribute
{
  // 어트리뷰트 필드 데이터
  public String Name { get; set; }
  // 생성자
  public DefaultData(String name)
  {
    this.Name = name;
  }
}
// Method 타입의 어트리뷰트 생성
[AttributeUsage(AttributeTargets.Method)]
class RunFlag : Attribute
{

}

// Node 클래스에 DefaultData 어트리뷰트 설정
[DefaultData("Hello world")]
class Node
{
  // Print 함수에 RunFlag 어트리뷰트 추가
  [RunFlag]
  public void Run()
  {
    Print();
  }
  public void Print()
  {
    // 클래스 객체의 타입을 가져온다.
    Type clz = this.GetType();
    // 클래스의 어트리뷰트를 가져온다.
    foreach (dynamic obj in clz.GetCustomAttributes(false))
    {
      // 어트리뷰트 중에 DefaultData 타입이면,
      if (obj.GetType() == typeof(DefaultData))
      {
        // 설정 Name값을 출력한다.
        Console.WriteLine(obj.Name);
      }
    }
  }
}
class Program
{
  static void Main(string[] args)
  {
    // 인스턴스 생성
    var node = new Node();
    // Node 클래스의 메서드의
    typeof(Node).GetMethods()
          // 어트리뷰트가 RunFlag가 있는 함수를 필터
          .Where(x => x.GetCustomAttributes(false).Where(y => y.GetType() == typeof(RunFlag)).Any())
          // 리스트로 출력한 후
          .ToList()
          .ForEach(x =>
          {
            // 함수명 출력
            Console.WriteLine("Execute - " + x.Name);
            // 함수를 실행
            x.Invoke(node, null);
          });
    // 아무 키나 누르시면 종료합니다.
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

실제로는 이 어트리뷰트의 타입으로 함수의 실행 여부나 순서를 설정하는 경우가 많이 있습니다.

예를 들면, 웹 프로젝트에서 어트리뷰트를 설정하면, 접속하는 유저의 권한에 따라 함수를 실행할 수 있는 지 없는 지에 대한 검사, 데이터에 따른 함수의 실행 순서를 설정 등을 할 수 있습니다.

using System;
using System.Linq;

// Method 타입의 어트리뷰트 생성
[AttributeUsage(AttributeTargets.Method)]
class ExecuteOrder : Attribute
{
  // 순서 맴버 변수
  public int Order { get; set; }
  // 생성자 (필수 설정)
  public ExecuteOrder(int order)
  {
    this.Order = order;
  }
}
// 예제 클래스
class Node
{
  // 함수에 어트리뷰트 설정
  [ExecuteOrder(3)]
  public void Test1()
  {
    // 콘솔 출력
    Console.WriteLine("Test1");
  }
  // 함수에 어트리뷰트 설정
  [ExecuteOrder(1)]
  public void Test2()
  {
    // 콘솔 출력
    Console.WriteLine("Test2");
  }
  // 함수에 어트리뷰트 설정
  [ExecuteOrder(4)]
  public void Test3()
  {
    // 콘솔 출력
    Console.WriteLine("Test3");
  }
  // 함수에 어트리뷰트 설정
  [ExecuteOrder(2)]
  public void Test4()
  {
    // 콘솔 출력
    Console.WriteLine("Test4");
  }
}
class Program
{
  static void Main(string[] args)
  {
    // 인스턴스 생성
    var node = new Node();
    // Node 클래스의 메서드의
    typeof(Node).GetMethods()
          // 어트리뷰트가 RunFlag가 있는 함수를 필터
          .Where(x => x.GetCustomAttributes(false).Where(y => y.GetType() == typeof(ExecuteOrder)).Any())
          // 어트리뷰트의 Order 값에 따른 정렬
          .OrderBy(x => (x.GetCustomAttributes(false).Where(y => y.GetType() == typeof(ExecuteOrder)).First() as ExecuteOrder).Order)
          // 리스트로 출력한 후
          .ToList()
          .ForEach(x =>
          {
            // 함수명 출력
            Console.WriteLine("Execute - " + x.Name);
            // 함수를 실행
            x.Invoke(node, null);
          });
    // 아무 키나 누르시면 종료합니다.
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 예제는 Node 클래스에 ExecuteOrder가 설정된 함수를 가져와서 실행 순서가 설정된 순서대로 함수가 실행됩니다.

위 예제는 제가 임의로 어트리뷰트에 int형을 넣어서 순서를 설정했습니다만, 데이터 베이스나 유저의 입력되는 값에 의해 순서를 결정하게 되면 그것이 바로 인터프리터 패턴(interpreter pattern)이 됩니다.


어트리뷰트는 위의 예제처럼 Reflection의 동적 실행을 위해서 사용하는 경우가 많이 있습니다. 사실 단순히 메타 데이터를 위한 것이라면 그냥 주석을 사용하는 게 더 깔끔합니다.

Reflection과 어트리뷰트를 잘 이용한다면, 여러가지 디자인 패턴의 알고리즘을 작성할 수 있고, 크게는 이런식으로 프레임워크를 작성할 수 있습니다.


여기까지 C#에서 Reflection 기능을 사용하는 방법 - Attribute에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.