상세 컨텐츠

본문 제목

[C#] 리플렉션과 애트리뷰트

.NET/C#

by 클리엘 클리엘 2021. 10. 20. 16:35

본문

728x90

1. 리플렉션

 

리플렉션은 프로그램 내부의 객체를 대상으로 형식(Type) 확인이 가능한 것을 말하는 것을 말합니다. 이것으로 해당 객체의 인스턴스를 만들거나 심지어는 인스턴스에 소속된 메서드를 호출할 수 있는 등의 동적이면서 유연한 활용이 가능합니다.

 

객체의 형식정보는 Object에 있는 GetType() 메서드를 통해 확인이 가능한데 이는 모든 데이터형 식이 이 Object를 상속하고 있으므로 어떠한 데이터 형식에서도 GetType() 메서드를 호출할 수 있다는 것을 의미합니다. 이 메서드에서는 Type형식을 반환하고 있고 이 형식을 통해 형식의 이름과 메서드, 프로퍼티 목록 등 객체를 이루는 거의 모든 것을 확인할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        string s = "hello";

        Type type = s.GetType();

        foreach (var item in type.GetMethods())
        {
            WriteLine(item.Name); //Type형식을 통해 string객체의 모든 메서드를 확인
        }
    }
}

예제에서는 메서드의 목록을 확인하기 위해 GetMethods() 메서드를 호출했는데 다른 메서드를 사용하면 그에 해당하는 정보를 확인할 수 있습니다.

GetConstructors() ConstructorInfo[] 생성자 목록
GetEvents() EventInfo[] 이벤트 목록
GetFields() FieldInfo[] 필드 목록
GetGenericArguments() Type[] 형식 매개변수 목록
GetInterfaces() Type[] 인터페이스 목록
GetMembers() MemberInfo[] 멤버 목록
GetMethods() MethodInfo[] 메서드 목록
GetProperties() PropertyInfo[] 프로퍼티 목록

사용가능한 더 자세한 메서드는 다음 링크를 참고하시면 됩니다.

Type 클래스 (System) | Microsoft Docs

 

Type 클래스 (System)

클래스 형식, 인터페이스 형식, 배열 형식, 값 형식, 열거형 형식, 형식 매개 변수, 제네릭 형식 정의 및 개방형 생성 제네릭 형식이나 폐쇄형 생성 제네릭 형식에 대한 형식 선언을 나타냅니다.Re

docs.microsoft.com

string형식의 Type정보를 얻기 위해 string형식의 인스턴스를 생성해 GetType() 메서드를 호출했는데 이렇게 인스턴스를 사용하지 않고 형식으로부터 바로 Type정보를 얻으려면 typeof()나

Type type = typeof(string);

Type.GetType() 메서드를 사용할 수 있습니다.

Type type = Type.GetType("System.String");

일부 메서드는 함수를 호출할때 검색 옵션을 지정하는 것도 가능합니다.

foreach (var item in type.GetMethods(System.Reflection.BindingFlags.Public))
{
    WriteLine(item.Name);
}

검색 옵션은 System.Reflection.BindingFlags 열거형을 이용하며 동시에 하나이상의 옵션을 같이 지정할 수도 있습니다.

//public 이면서 static 인것
foreach (var item in type.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
{
    WriteLine(item.Name);
}

만약 아무런 옵션도 지정하지 않으면 기본적으로 public으로 검색합니다.

 

이러한 방법으로 형식 정보를 얻는 것뿐만이 아니라 특정 형식의 인스턴스를 생성해 해당 객체에 속한 메서드 등을 호출할 수도 있습니다.

 

우선 Type객체로 부터 인스턴스를 생성하려면 System.Activator클래스를 사용해야 하는데

class Program
{
    static void Main(string[] args)
    {
        Type type = typeof(string);

        object o = Activator.CreateInstance(type);
    }
}

이때 CreateInstance()메서드가 사용되며 메서드는 지정한 형식으로부터 생성한 인스턴스를 반환하도록 합니다. 참고로 메서드는 일반화 버전도 가능하며 이를 통해 List<string>형식의 인스턴스를 얻고자 한다면 다음과 같이 형식을 지정해 주면 됩니다.

object o = Activator.CreateInstance<List<string>>();

이제 인스턴스를 생성하였으므로 이를 통해서 특정 프로퍼티에 임의의 값을 부여하거나 해당 프로퍼티를 통해서 값을 확인할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        Type type = typeof(Car);
        object o = Activator.CreateInstance(type);

        PropertyInfo speed = type.GetProperty("Speed");
        PropertyInfo color = type.GetProperty("Color");

        speed.SetValue(o, 80);
        color.SetValue(o, "Red");

        WriteLine(speed.GetValue(o));
        WriteLine(color.GetValue(o));
    }
}

class Car
{
    public int Speed { get; set; }
    public string Color { get; set; }
}

프로퍼티에 값을 할당하고 가져오는데는 PropertyInfo를 사용하였으며 SetValue()로 값을 설정하고 GetValue()로 설정된 값을 가져오고 있습니다. 참고로 인덱서 사용이 필요한 경우 SetValue()에서 세 번째 매개변수, 그리고 GetValue()에서 두 번째 매개변수에 인덱서를 지정합니다.

 

메서드의 호출에서는 MethodInfo클래스가 사용되며 Invoke()메서드를 통해 원하는 메서드를 호출할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        Type type = typeof(Car);
        object o = Activator.CreateInstance(type);

        MethodInfo method = type.GetMethod("Drive");

        method.Invoke(o, null);
    }
}

class Car
{
    public int Speed { get; set; }
    public string Color { get; set; }

    public void Drive()
    {
        WriteLine("주행시작");
    }
}

만약 메서드를 호출할때 매개변수가 필요하다면 Invoke() 메서드에서 null대신 매개변수의 인수를 지정해 줍니다.

 

이제까지 형식을 가져와 인스턴스를 생성하고 프로퍼티에 값을 할당하거나 메서드를 호출하는 등의 시도를 해보았는데 C#에서는 기존의 형식을 사용하는 것이 아닌 동적으로 새로운 형식을 만들어 해는 것도 가능합니다. 이때 사용되는 클래스가 System.Reflection.Emit 네임스페이스에 속한 클래스들이며 각각마다 특정한 요소를 만들어 내는 역할을 합니다.

AssemblyBuilder 어셈블리
ConstructorBuilder 생성자
CustomAttributeBuilder 애트리뷰트
EnumBuilder 열거형
EventBuilder 이벤트
FieldBuilder 필드
GenericTypeParameterBuilder 일반화 형식 매개변수
LocalBuilder 지역 변수
MethodBuilder 메서드
ModuleBuilder 모듈
ParameterBuilder 매개변수
PropertyBuilder 프로퍼티
TypeBuilder 클래스
ILGenerator IL코드를 생성합니다.

위 클래스들로 새로운 형식을 만들려면 어셈블리부터 시작해 메서드/프로퍼티까지 계층적인 구조를 따라 순서대로 진행되어야 합니다. 이 구조는 통상 .NET프로그램의 계층구조와 동일한 구조를 갖습니다.

 

가장 먼저 AssemblyBuilder를 통해 어셈블리를 만듭니다.

AssemblyBuilder calAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("calAssembly"), AssemblyBuilderAccess.Run);

AssemblyBuilder는 생성자가 없으므로 DefineDynamicAssembly() 메서드를 사용해 인스턴스를 생성해야 합니다. 어셈블리를 만들고 나면 그다음은 모듈을 생성합니다.

AssemblyBuilder calAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("calAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder calModule = calAssembly.DefineDynamicModule("calModule");

모듈은 어셈블리 내부에 생성되므로 AssemblyBuilder의 DefineDynamicModule()메서드를 사용해 모듈을 생성합니다. 모듈까지 완성되었으면 다음으로 클래스를 생성합니다.

AssemblyBuilder calAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("calAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder calModule = calAssembly.DefineDynamicModule("calModule");
TypeBuilder calType = calModule.DefineType("MyCal");

클래스는 모듈안에 있으므로 역시 ModuleBuilder의 DefineType메서드를 통해 클래스를 생성합니다. 클래스를 생성하면 이번에는 클래스 내부에 들어갈 메서드를 만듭니다.

AssemblyBuilder calAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("calAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder calModule = calAssembly.DefineDynamicModule("calModule");
TypeBuilder calType = calModule.DefineType("MyCal");
MethodBuilder calMethod = calType.DefineMethod("Adder", MethodAttributes.Public, typeof(int), new Type[2]);

예제에서의 메서드는 Sum이라는 메서드이며 public 접근성을 가지고 있습니다. 또한 int형식의 값을 반환하고 매개변수는 2개를 필요로 합니다.

 

위에서 처럼 메서드를 만들었으니 메서드내부에 동작할 코드를 채워야 합니다. 이 작업은 ILGenerator객체를 사용해 이루어지는데 사실상 이 부분이 제일 핵심적인 부분이며 동시에 제일 어려운 부분이기도 합니다.

AssemblyBuilder calAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("calAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder calModule = calAssembly.DefineDynamicModule("calModule");
TypeBuilder calType = calModule.DefineType("MyCal");
MethodBuilder calMethod = calType.DefineMethod("Adder", MethodAttributes.Public, typeof(int), new Type[] { typeof(int), typeof(int) });

ILGenerator ILGen = calMethod.GetILGenerator();
ILGen.Emit(OpCodes.Ldarg_1);
ILGen.Emit(OpCodes.Ldarg_2);
ILGen.Emit(OpCodes.Add);
ILGen.Emit(OpCodes.Ret);
calType.CreateType();

Ldarg_1은 첫번째 매개변수를 Ldag_2는 두 번째 매개변수의 값을 스택에 올립니다.(Ldarg_0은 메서드의 선언부에 해당합니다.) 만들려고 하는 메서드는 2개의 정수형 인수를 받아 그 값을 더하고 결과를 반환하는 메서드입니다. 따라서 인수로 전달되는 값을 순서대로 스택에 올려야 하는 것입니다. 그런 뒤 Add를 통해 스택에 있는 값을 더하고 그 결과를 다시 스택에 저장하며 저장된 값을 Rect를 통해 반환하는 것으로 메서드의 본문을 만들었습니다. 이러한 방법으로 IL코드를 완성했으면 CreateType() 메서드를 호출해 위에서 부터 만들어온 코드를 CLR에 제출합니다.

 

이제 새롭게 완성된 형식은 다음과 같이 인스턴스를 생성하여 메서드를 호출하고 그 결과를 받아볼 수 있습니다.

AssemblyBuilder calAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("calAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder calModule = calAssembly.DefineDynamicModule("calModule");
TypeBuilder calType = calModule.DefineType("MyCal");
MethodBuilder calMethod = calType.DefineMethod("Adder", MethodAttributes.Public, typeof(int), new Type[] { typeof(int), typeof(int) });

ILGenerator ILGen = calMethod.GetILGenerator();
ILGen.Emit(OpCodes.Ldarg_1);
ILGen.Emit(OpCodes.Ldarg_2);
ILGen.Emit(OpCodes.Add);
ILGen.Emit(OpCodes.Ret);
calType.CreateType();

object o = Activator.CreateInstance(calType);
MethodInfo cal = o.GetType().GetMethod("Adder");
object[] aVargs = new object[] { 10, 20 };
WriteLine(cal.Invoke(o, aVargs));

2. 특성 (애트리뷰트)

 

애트리뷰트 혹은 특성이라고 하는 것은 코드자체에 필요한 정보를 부여하여 이를 메타데이터로 활용 위한 것입니다. 예를 들어 어떤 메서드에 대해 해당 메서드를 사용하는 사람으로부터 특별한 메시지를 전달하고자 한다면 Obsolete라는 애트리뷰트를 사용할 수 있습니다.

class Car
{
    [Obsolete("한번 달리면 멈출 수 없어!!")]
    public void Drive()
    {

    }
}

이 상태에서 만약 다른 개발자가 Car클래스의 Drive()메서드를 호출하면 다음과 같은 화면을 보게 됩니다.

혹은 C/C++로 만들어진 DLL모듈의 함수를 호출하기 위해 다음과 같이 사용되는 경우도 있습니다.

[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();

이러한 방법으로 애트리뷰트는 코드상에서 여러가지 형태의 정보를 부여하고 코드를 사용하고자 하는 사람들에게 필요한 정보를 전달하거나 특정 기능을 제공하는 용도로 사용됩니다.

 

물론 설정을 통한 애트리뷰트 사용뿐 아니라 기본적으로 제공되는 애트리뷰트를 사용할 수도 있는데 그 중에서 호출자 정보 애트리뷰트는 메서드를 호출한 호출자 이름, 파일 경로, 줄번호 등의 상세정보를 확인할 수 있게 하는 애트리뷰트입니다.

class MyClass
{
    public void LogCall()
    {
        Log("에러 발생!!");
    }

    private void Log(string message, [CallerFilePath] string file = "", [CallerLineNumber] int line = 0)
    {
        WriteLine($"로그내용 - {message} / {file}에서 {line}번 라인에서 호출");
    }
}

호출자 정보 애트리뷰트는 위와 같이 메서드에서 인수로 애트리뷰트를 지정해 주기만 하면 됩니다. 그러면 해당 메서드를 호출할때 파일 경로나 줄번호와 같은 정보를 명시하지 않아도 알아서 해당 정보가 전달됩니다.

CallerFilePath 호출자의 파일 경로
CallerMemberName 호출자의 메서드이름
CallerLineNumber 호출자의 소스코드상 라인번호

C#에서는 다양하게 제공되는 애트리뷰트가 있지만 마음에 드는 애트리뷰트가 없는 경우 직접 만들어 사용할 수도 있습니다.

class MyAttribute : System.Attribute
{
    
}

애트리뷰트도 하나의 클래스이며 System.Attribute클래스를 상속받아야 합니다. 이렇게 하는 것만으로 나만의 애트리뷰트가 만들어 지는데 이 상태에서 필요한 값을 설정하고 처리하는 내부만 채워주면 됩니다.

class MyAttribute : System.Attribute
{
    public string Message
    {
        get;set;
    }

    public MyAttribute(string message)
    {
        Message = message;
    }
}

예제는 단순히 문자열내용을 받아 설정하는 애트리뷰트를 나타내고 있습니다. 이 애트리뷰트를 메서드에서 사용하려면 다음과 같이 해줄 수 있습니다.

[MyAttribute("가가호호")]
static void attributeTest()
{
    
}

보시는 바와 같이 애트리뷰트를 사용하기 위해 애트리뷰트 이름을 지정하면 내부적으로 생성자를 호출하게 되고 때문에 생성자에 맞게 생성자 매개변수로 값을 설정해주면 됩니다. 생성자뿐만 아니라 별도의 속성도 마련되어 있으므로 필요하다면 속성을 개별적으로 설정합니다.

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

    [MyAttribute("가가호호", Message = "ABC")]
    static void attributeTest()
    {
        
    }
}

애트리뷰트를 만들 때는 또 다른 애트리뷰트의 도움을 받아 해당 애트리뷰트의 기능을 확장할 수도 있습니다. 예를 들어 현재 MyAttribute는 단 한 번만 사용 가능한데 중복해서 여러 번 사용하려면 System.AttributeUsage 애트리뷰트를 사용해 문제를 해결할 수 있습니다.

[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true)]
class MyAttribute : System.Attribute
{
    public string Message
    {
        get;set;
    }

    public MyAttribute(string message)
    {
        Message = message;
    }
}

예제에서는 MyAttribute가 Method에서 중복해서 사용가능함을 System.AttributeUsage애트리뷰트로 지정하고 있습니다.

 

참고로 AttributeTargets로 지정할 수 있는 설정대상은 아래 글을 참고해 주시기 바랍니다.

특성(C#) | Microsoft Docs

 

특성(C#)

특성을 사용하여 C#에서 메타데이터 또는 선언적 정보를 코드와 연결하는 방법에 대해 알아봅니다. 특성은 리플렉션을 사용하여 런타임에 쿼리할 수 있습니다.

docs.microsoft.com

만약 설정대상이 여러 개인 경우는 | 연산자를 통해 결합해 줄 수 있습니다.

[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple = true)]
728x90

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

[C#] 파일과 디렉터리 다루기  (0) 2021.10.21
[C#] dynamic 형식  (0) 2021.10.21
[C#] 리플렉션과 애트리뷰트  (0) 2021.10.20
[C#] LINQ  (0) 2021.10.19
[C#] 람다식  (0) 2021.10.19
[C#] 대리자와 이벤트  (0) 2021.10.18

관련글 더보기

댓글 영역