C# 리플렉션

2025. 2. 23. 23:54개발/C#

이 글은 박상현님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.


 

제가 생각하는 컴파일타임과 런타임의 차이는 정적과 동적으로 구분됩니다. 컴파일타임은 최적화를 위해 정적인 요소를 확정 짓는다고 생각합니다. 매크로, 스택 메모리 크기, 자료형 등 변하지 않는 값들은 확정되어 프로그램의 동작에 최적화와 안정성을 보장합니다. 그리고 런타임은 프로그램의 실행을 뜻하며, 사용자의 입력 등으로 변하는 동적값들이 런타임에 사용되어 사용자가 원하는 작업을 처리할 수 있게 해줍니다. 그렇다면 런타임에 사용자의 요청으로 컴파일타임에 확정되었을 클래스와 메서드의 구조를 새롭게 생성하는 것은 가능할까요?

 

리플렉션, Reflection

Runtime에서 객체의 정보를 확인 및 조작 할 수 있는 기능을 뜻합니다. 이 기능을 이용하면 실행중인 프로그램의 객체 형식 이름, 프로퍼티 목록, 메서도 목록, 이벤트 등 모든 메타 데이터를 조회할 수 있으며, 형식의 이름을 알고 있다면 동적으로 인스턴스를 생성할 수도 있습니다.

 

우선 Object.GetType 메서드를 사용하여 객체의 정보를 확인해보겠습니다.

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        int integer = 0;

        Type type = integer.GetType();
        FieldInfo[] fields = type.GetFields();

        foreach (FieldInfo field in fields)
        {
            Console.WriteLine($"Type: {field.FieldType}, Name: {field.Name}");
        }
    }
}

 

Type: System.Int32, Name: MaxValue
Type: System.Int32, Name: MinValue

 

위 예시는 int 객체의 필드를 출력하는 코드 입니다. GetType으로 int 객체의 정보를 담은 Type 객체를 가져와 GetFields 메서드로 public 필드를 조회하여, int 객체 필드의 형식과 이름을 출력결과로 확인할 수 있었습니다.

 

모든 객체은 Object를 상속받아 GetType 메서드를 사용할 수 있습니다. 이때 반환값은 Type 객체를 사용합니다. Type 객체는 .NET에서 사용되는 데이터의 형식 정보를 모두 담고 있습니다. 앞서 말한 형식 이름, 프로퍼티 목록, 메서드 목록과 같은 메타 데이터들을 말이죠.

 

당연히 사용자가 생성한 클래스도 Type 객체를 사용할 수 있습니다.

using System;
using System.Reflection;

class Game
{
    public string Name { get; set; } = "Game";
    public string Description { get; set; }

    public void Play() => Console.WriteLine($"{Name} Start!");
}

class Program
{
    static void Main()
    {
        Type type = typeof(Game);
        PropertyInfo[] properties = type.GetProperties();
        MethodInfo[] methods = type.GetMethods();

        Console.WriteLine($"Class: {type.Name}");
        Console.WriteLine($"Namespace: {type.Namespace}");
        Console.WriteLine($"Properties: {properties.Length}");
        foreach (var property in properties)
        {
            Console.WriteLine($"    Name: {property.Name}, Type: {property.PropertyType}");
        }
        Console.WriteLine($"Methods: {methods.Length}");

        foreach (var method in methods)
        {
            Console.WriteLine($"    Name: {method.Name}, Return Type: {method.ReturnType}");
        }
    }
}

 

Class: Game
Namespace:
Properties: 2
    Name: Name, Type: System.String
    Name: Description, Type: System.String
Methods: 9
    Name: get_Name, Return Type: System.String
    Name: set_Name, Return Type: System.Void
    Name: get_Description, Return Type: System.String
    Name: set_Description, Return Type: System.Void
    Name: Play, Return Type: System.Void
    Name: GetType, Return Type: System.Type
    Name: ToString, Return Type: System.String
    Name: Equals, Return Type: System.Boolean
    Name: GetHashCode, Return Type: System.Int32

 

위 예시는 Game 클래스를 작성하여 메타 데이터를 조회하는 코드입니다. 이번에는 GetType이 아닌 typeof 연산자를 사용하여 Type을 반환 받았습니다. typeof 연산자는 자원을 할당한 객체가 아닌 클래스를 통해서 Type을 생성하는 연산자 입니다. 그 다음 이번에는 필드가 아닌 프로퍼티와 메서드 목록을 불러와서 출력하였습니다.

 

Type에는 이 외에도 이벤트 목록 상속받은 인터페이스 목록 등 다양한 목록을 메서드를 통해 조회할 수 있습니다. 자세한 메서드 목록은 Microsoft Learn을 통해 확인할 수 있습니다.

 

Activator

이 클래스는 Type을 매개변수로 받아 동적으로 해당 Type 객체 기반으로 인스턴스를 생성할 수 있게 해줍니다.

using System;
using System.Reflection;

class Game
{
    public string Name { get; set; } = "Game";
    public string Description { get; set; }

    public void Play() => Console.WriteLine($"{Name} Start!");
}

class Program
{
    static void Main()
    {
        Type type = typeof(Game);

        object gameInstance = Activator.CreateInstance(type);

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

        method.Invoke(gameInstance, null);

        PropertyInfo name = type.GetProperty("Name");

        name.SetValue(gameInstance, "Hangman");

        method.Invoke(gameInstance, null);
    }
}

 

Game Start!
Hangman Start!

 

위 예시는 Activatior를 통해 동적으로 Game 인스턴스를 생성하고 Play 메서드를 가져와 실행하는 코드입니다. 그리고 중간에 Game 인스턴스안의 프로퍼티도 가져와 Name 프로퍼티를 변경 후 Play 메서드 다시 실행해 정상적으로 적용되었는지 확인하였습니다.

 

InvokeMethodBase 추상 클래스에서 상속 받은 메서드로 첫 번째 매개변수로 메서드를 호출하는 객체를 전달하고 두번째 매개변수로 메서드에 사용될 매개변수 배열을 전달합니다. 이 메서드를 통해 Play 메서드를 실행할 수 있습니다.

 

프로퍼티와 메서드 동일하게 형식의 이름을 통해 찾을 수 있으며, 만약 일치하는 이름이 없을 경우 null을 반환합니다. 프로퍼티도 SetValue 메서드를 통해 Set을 실행할 수 있습니다.

 

Emit

이 기능은 런타임 중에 새로운 형식을 만들 수 있게 지원합니다. Emit은 내보낸다는 의미로, 프로그램이 실행중 CLR 메모리에 새 형식을 내보낸다는 뜻으로 이해할 수 있습니다. 기본적으로 새로운 형식을 만드는 순서는 아래와 같습니다.

  1.  AssemblyBuilder 클래스를 이용하여 어셈블리를 생성합니다.
  2. ModuleBuilder 클래스를 이용해 1. 에서 생성한 어셈블리 안에 모듈을 생성합니다.
  3. TypeBuilder 클래스를 이용해 2. 에서 생성한 모듈 안에 클래스를 생성합 니다.
  4. MthodBuilder 혹은 PropertyBuilder 클래스를 이용하여 3. 에서 생성한 클래스 안에 메서드 혹은 프로퍼티를 생성합니다.
  5. 만약 메서드를 생성하였다면, ILGenerator 클래스를 이용하여 4. 에서 생성한 메서드안에 CPU가 실행 할 IL(Intermediate Language) 명령어를 넣습니다.

.NET 프로그램 계층 구조

 

 

위 순서를 이해하고 다음 예시를 통해 Emit 기능을 확인해보겠습니다.

using System;
using System.Reflection;
using System.Reflection.Emit;

class Program
{
    static void Main()
    { 
        AssemblyName assemblyName = new AssemblyName("Assembly");

        AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");

        TypeBuilder typeBuilder = moduleBuilder.DefineType("MyClass", TypeAttributes.Public);

        MethodBuilder methodBuilder = typeBuilder.DefineMethod(
            "Hello",
            MethodAttributes.Public,
            typeof(void),
            Type.EmptyTypes
        );

        ILGenerator il = methodBuilder.GetILGenerator();
        il.Emit(OpCodes.Ldstr, "Hello, World!");
        il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
        il.Emit(OpCodes.Ret);

        
        Type dynamicType = typeBuilder.CreateType();

        object myInstance = Activator.CreateInstance(dynamicType);

        MethodInfo helloMethod = dynamicType.GetMethod("Hello");

        helloMethod.Invoke(myInstance, null);
    }
}

 

Hello, World!

 

위 예시는 MyClass 라는 클래스 안에 Hello라는 메서드를 생성하여 호출하는 코드 입니다. 우선 Assembly를 생성합니다. 그 다음 ModuleClass를 생성한 후 Method를 생성합니다. 위 코드들을 보시면 아시겠지만 모든 계층 클래스가 앞서 만든 객체의 메서드로 생성되는 팩토리 구조형태를 이루고 있습니다.

 

이 후 생성된 메서드 안에 IL 코드를 넣습니다. 내부의 OpCodesIL 명령어를 사용할 수 있게 해주는 클래스입니다. 문자열을 스택에 넣고 Console.WriteLine의 메서드를 GetMethod를 통해 가져와 호출한 뒤 값을 반환할 수 있게 하였습니다.

 

이후 TypeBuilderCreateType으로 MyClassType을 받아 Activator를 통해 인스턴스를 생성하여 Hello 메서드를 호출하였습니다.

 

참고로 프로퍼티는 생성한 뒤 개별적으로 get set 프로퍼티 메서드를 만들어 주어야합니다.

 

'개발 > C#' 카테고리의 다른 글

C# dynamic  (0) 2025.03.05
C# 애트리뷰트  (0) 2025.02.25
C# LINQ  (0) 2025.02.21
C# 이벤트  (0) 2025.02.20
C# 대리자, 익명 메서드 그리고 람다식  (0) 2025.02.18