Reflection

Reflection

Reflection
Published

April 24, 2025

Abstract

C#의 강력한 기능인 리플렉션(Reflection)과 애트리뷰트(Attribute)에 대해 자세히 알아봅니다. 리플렉션을 사용하면 런타임에 형식(Type) 정보를 탐색하고 조작할 수 있으며, 애트리뷰트를 사용하면 코드에 메타데이터를 추가하여 프로그램의 동작을 확장하거나 정보를 제공할 수 있습니다.

리플렉션(Reflection)은 런타임에 프로그램의 메타데이터를 검사하고 상호 작용하는 기능을 제공합니다. 이를 통해 어셈블리, 모듈, 형식(클래스, 인터페이스, 구조체, 대리자, 열거형 등) 및 해당 멤버(메소드, 속성, 필드, 이벤트, 생성자 등)에 대한 정보를 동적으로 얻고, 심지어는 형식의 인스턴스를 만들거나 메소드를 호출할 수도 있습니다. System.Reflection 네임스페이스의 클래스들을 주로 사용합니다.

Object.GetType() 메소드와 Type 클래스

모든 C# 형식의 기반이 되는 System.Object 클래스에는 GetType() 메소드가 정의되어 있습니다. 이 메소드는 객체의 런타임 형식을 나타내는 System.Type 객체를 반환합니다. System.Type 클래스는 리플렉션의 핵심으로, 특정 형식에 대한 모든 정보(메타데이터)에 접근할 수 있는 통로를 제공합니다. Type 객체를 얻는 주요 방법은 다음과 같습니다.

  • Object.GetType(): 객체 인스턴스에서 런타임 형식을 가져옵니다.
  • typeof() 연산자: 컴파일 타임에 클래스 이름을 지정하여 해당 Type 객체를 가져옵니다.
  • Type.GetType()(정적 메소드): 형식의 전체 이름(네임스페이스 포함) 문자열을 사용하여 Type 객체를 가져옵니다. 로드되지 않은 어셈블리의 형식을 가져올 수도 있습니다.
using System;
using System.Reflection; // 리플렉션 관련 클래스 사용

public class MyClass
{
    public int MyProperty { get; set; }
    public void MyMethod() { }
}

public class ReflectionExample
{
    public static void Main(string[] args)
    {
        // 1. Object.GetType() 사용
        MyClass myObj = new MyClass();
        Type typeFromInstance = myObj.GetType();
        Console.WriteLine($"[Object.GetType()] 형식 이름: {typeFromInstance.Name}"); // 출력: MyClass

        // 2. typeof() 연산자 사용
        Type typeFromClass = typeof(MyClass);
        Console.WriteLine($"[typeof()] 형식 이름: {typeFromClass.Name}"); // 출력: MyClass
        Console.WriteLine($"[typeof()] 전체 이름: {typeFromClass.FullName}"); // 출력: ReflectionDemo.MyClass
        Console.WriteLine($"[typeof()] 기본 형식: {typeFromClass.BaseType}"); // 출력: System.Object

        // 3. Type.GetType() 사용 (정적 메소드, 전체 이름 필요)
        Type typeFromString = Type.GetType("ReflectionDemo.MyClass, ReflectionDemo"); // 네임스페이스와 어셈블리 이름 지정
        if (typeFromString != null)
        {
            Console.WriteLine($"[Type.GetType()] 형식 이름: {typeFromString.Name}"); // 출력: MyClass
        }

        // Type 객체를 이용한 멤버 정보 조회
        Console.WriteLine("\n--- MyClass 멤버 정보 ---");
        System.Reflection.MemberInfo[] members = typeFromClass.GetMembers(); // 모든 public 멤버 가져오기
        foreach (System.Reflection.MemberInfo member in members)
        {
            Console.WriteLine($"{member.MemberType}: {member.Name}");
        }

        Console.WriteLine("\n--- MyClass 메소드 정보 ---");
        System.Reflection.MethodInfo[] methods = typeFromClass.GetMethods(); // 모든 public 메소드 가져오기
        foreach (System.Reflection.MethodInfo method in methods)
        {
            // 상속된 메소드(예: ToString, Equals)도 포함됨
            if (method.DeclaringType == typeFromClass) // MyClass에서 직접 정의한 메소드만 필터링 (선택적)
            {
                Console.WriteLine($"메소드: {method.ReturnType.Name} {method.Name}()");
            }
        }
    }
}

리플렉션을 이용해서 객체 생성하기

리플렉션을 사용하면 컴파일 시점에 특정 클래스를 알지 못하더라도, 런타임에 해당 클래스의 이름을 문자열 등으로 받아 인스턴스를 동적으로 생성할 수 있습니다. 주로 System.Activator 클래스의 CreateInstance 메소드나, ConstructorInfo 객체를 얻어 Invoke 메소드를 사용하는 방법이 있습니다.

  • Activator.CreateInstance 사용: 가장 간단한 방법이며, 주로 매개변수 없는 기본 생성자를 호출할 때 사용됩니다. 매개변수가 있는 생성자를 호출할 수도 있습니다.
using System;

public class Greeter
{
    private string message;

    // 기본 생성자
    public Greeter()
    {
        this.message = "안녕하세요!";
    }

    // 매개변수 있는 생성자
    public Greeter(string message)
    {
        this.message = message;
    }

    public void SayHello()
    {
        Console.WriteLine(message);
    }
}

public class DynamicInstantiation
{
    public static void Main(string[] args)
    {
        // 1. 기본 생성자를 이용한 객체 생성
        Type greeterType = typeof(Greeter);
        object greeterObj1 = Activator.CreateInstance(greeterType);

        if (greeterObj1 is Greeter g1)
        {
            g1.SayHello(); // 출력: 안녕하세요!
        }

        // 2. 매개변수 있는 생성자를 이용한 객체 생성
        object greeterObj2 = Activator.CreateInstance(greeterType, new object[] { "반갑습니다!" });

        if (greeterObj2 is Greeter g2)
        {
            g2.SayHello(); // 출력: 반갑습니다!
        }
    }
}
  • ConstructorInfo.Invoke 사용: 특정 생성자를 명시적으로 선택하여 호출하고 싶을 때 사용합니다.
using System;
using System.Reflection;

// Greeter 클래스는 위 예제와 동일하다고 가정

public class DynamicInstantiationWithConstructor
{
    public static void Main(string[] args)
    {
        Type greeterType = typeof(Greeter);

        // 1. 기본 생성자 정보 얻기 및 호출
        ConstructorInfo defaultCtor = greeterType.GetConstructor(Type.EmptyTypes); // 매개변수 없는 생성자
        if (defaultCtor != null)
        {
            object greeterObj1 = defaultCtor.Invoke(null); // 매개변수 없으므로 null 전달
            if (greeterObj1 is Greeter g1)
            {
                g1.SayHello(); // 출력: 안녕하세요!
            }
        }

        // 2. 매개변수 있는 생성자 정보 얻기 및 호출
        ConstructorInfo paramCtor = greeterType.GetConstructor(new Type[] { typeof(string) }); // string 타입 매개변수 1개 받는 생성자
        if (paramCtor != null)
        {
            object greeterObj2 = paramCtor.Invoke(new object[] { "Hello Reflection!" }); // 매개변수 전달
            if (greeterObj2 is Greeter g2)
            {
                g2.SayHello(); // 출력: Hello Reflection!
            }
        }
    }
}

형식 내보내기 (어셈블리 메타데이터 검사)

리플렉션은 현재 실행 중인 코드뿐만 아니라 다른 .NET 어셈블리(.dll 또는 .exe 파일)의 메타데이터를 로드하고 검사하는 데에도 사용됩니다. 이를 통해 어셈블리가 포함하는 모든 형식(클래스, 인터페이스 등)과 그 멤버 정보를 동적으로 탐색할 수 있습니다. 주로 플러그인 시스템, 코드 분석 도구, 직렬화 라이브러리 등에서 활용됩니다.

System.Reflection.Assembly 클래스를 사용하여 어셈블리를 로드하고, GetTypes() 또는 GetExportedTypes() 메소드로 어셈블리 내의 형식 정보를 가져올 수 있습니다.

using System;
using System.Reflection;
using System.IO; // Path 클래스 사용

public class AssemblyInspector
{
    public static void Main(string[] args)
    {
        try
        {
            // 현재 실행 중인 어셈블리 로드
            Assembly currentAssembly = Assembly.GetExecutingAssembly();
            Console.WriteLine($"--- 현재 어셈블리 정보 ({currentAssembly.FullName}) ---");
            PrintAssemblyTypes(currentAssembly);

            // 다른 어셈블리 로드 (예: mscorlib 또는 특정 DLL 파일)
            // 주의: .NET Core/.NET 5+ 에서는 mscorlib 대신 System.Private.CoreLib 등을 사용
            // Assembly coreAssembly = Assembly.Load("System.Private.CoreLib");
            // Console.WriteLine($"\n--- CoreLib 어셈블리 정보 ({coreAssembly.FullName}) ---");
            // PrintAssemblyTypes(coreAssembly);

            // 특정 경로의 DLL 파일 로드 예시
            // string dllPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "MyLibrary.dll");
            // if (File.Exists(dllPath))
            // {
            //    Assembly customAssembly = Assembly.LoadFrom(dllPath);
            //    Console.WriteLine($"\n--- {customAssembly.GetName().Name} 어셈블리 정보 ---");
            //    PrintAssemblyTypes(customAssembly);
            // }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"어셈블리 로드 또는 검사 중 오류 발생: {ex.Message}");
        }
    }

    // 어셈블리 내의 public 형식 정보 출력하는 함수
    public static void PrintAssemblyTypes(Assembly assembly)
    {
        Console.WriteLine($"어셈블리 위치: {assembly.Location}");
        Type[] types = assembly.GetExportedTypes(); // 어셈블리 외부로 공개된 형식만 가져옴 (GetTypes()는 모든 형식)

        Console.WriteLine($"공개된 형식 ({types.Length}개):");
        foreach (Type type in types)
        {
            Console.WriteLine($"  - {type.FullName} ({type.BaseType?.Name ?? "N/A"})");

            // 형식의 public 메소드 정보 출력 (예시)
            // MethodInfo[] methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            // foreach (MethodInfo method in methods)
            // {
            //    Console.WriteLine($"      -> Method: {method.Name}");
            // }
        }
    }
}

애트리뷰트(Attribute)

애트리뷰트는 클래스, 메소드, 속성, 어셈블리 등 코드 요소에 추가적인 선언적 정보(메타데이터)를 연결하는 방법입니다. 애트리뷰트는 코드 자체의 실행 로직에는 직접적인 영향을 주지 않지만, 컴파일러, 런타임(리플렉션 사용), 또는 다른 도구에서 이 메타데이터를 읽어 특정 작업을 수행하도록 할 수 있습니다.

애트리뷰트는 대괄호([]) 안에 애트리뷰트 이름을 명시하여 코드 요소 바로 앞에 배치합니다. 많은 내장 애트리뷰트가 있으며, 사용자가 직접 정의하여 사용할 수도 있습니다.

애트리뷰트 사용하기

.NET에는 다양한 내장 애트리뷰트가 있습니다.

  • [Obsolete]: 특정 코드 요소(메소드, 클래스 등)가 더 이상 사용되지 않음을 표시합니다. 컴파일 시 경고 또는 오류를 발생시킬 수 있습니다.
  • [Serializable]: 클래스가 직렬화 가능함을 나타냅니다.
  • [DllImport]: 비관리 코드(DLL)의 함수를 호출할 때 사용됩니다.
  • [Conditional]: 특정 전처리기 심볼이 정의된 경우에만 메소드 호출이 컴파일되도록 합니다.
using System;
using System.Reflection;
using System.Diagnostics; // Conditional 애트리뷰트 사용

public class AttributeUsageExample
{
    [Obsolete("이 메소드는 구식이므로 NewMethod()를 사용하세요.", false)] // false: 경고만 발생, true: 오류 발생
    public void OldMethod()
    {
        Console.WriteLine("Old Method");
    }

    public void NewMethod()
    {
        Console.WriteLine("New Method");
    }

    [Conditional("DEBUG")] // DEBUG 심볼이 정의되었을 때만 호출이 포함됨
    public static void LogDebugMessage(string message)
    {
        Console.WriteLine($"[DEBUG] {message}");
    }

    public static void Main(string[] args)
    {
        AttributeUsageExample example = new AttributeUsageExample();
        example.OldMethod(); // 컴파일 시 경고 발생
        example.NewMethod();

        LogDebugMessage("디버그 메시지입니다."); // DEBUG 빌드에서는 출력됨, Release 빌드에서는 호출 자체가 사라짐

        // 리플렉션을 이용해 애트리뷰트 정보 읽기
        Type type = typeof(AttributeUsageExample);
        MethodInfo oldMethodInfo = type.GetMethod("OldMethod");

        // ObsoleteAttribute가 적용되었는지 확인
        Attribute attr = oldMethodInfo.GetCustomAttribute(typeof(ObsoleteAttribute));
        if (attr is ObsoleteAttribute obsoleteAttr)
        {
            Console.WriteLine($"\nOldMethod에는 Obsolete 애트리뷰트가 적용되었습니다.");
            Console.WriteLine($"메시지: {obsoleteAttr.Message}");
            Console.WriteLine($"오류 여부: {obsoleteAttr.IsError}");
        }
    }
}

리플렉션을 사용하여 코드 요소에 적용된 애트리뷰트를 런타임에 검사할 수 있습니다. MemberInfo.GetCustomAttributes(), Attribute.GetCustomAttribute() 등의 메소드를 사용합니다.

호출자 정보 애트리뷰트 (Caller Information Attributes)

C# 5.0부터 도입된 호출자 정보 애트리뷰트는 메소드의 선택적 매개변수에 적용되어, 해당 메소드를 호출하는 코드의 소스 파일 경로, 줄 번호, 멤버 이름을 컴파일 시점에 자동으로 전달받을 수 있게 해줍니다. 이는 주로 로깅, 디버깅, 진단 목적으로 유용하게 사용됩니다.

주요 호출자 정보 애트리뷰트는 다음과 같습니다 (System.Runtime.CompilerServices 네임스페이스에 정의됨).

  • [CallerMemberName]: 호출하는 멤버(메소드 또는 속성)의 이름을 제공합니다.
  • [CallerFilePath]: 호출하는 코드의 소스 파일 전체 경로를 제공합니다.
  • [CallerLineNumber]: 호출하는 코드의 소스 파일 내 줄 번호를 제공합니다.
using System;
using System.Runtime.CompilerServices; // 호출자 정보 애트리뷰트 사용

public class Logger
{
    // 각 매개변수에 기본값을 제공해야 하며, 호출 시 명시적으로 값을 전달하지 않아야 컴파일러가 자동으로 채워줌
    public static void Log(string message,
                           [CallerMemberName] string memberName = "",
                           [CallerFilePath] string sourceFilePath = "",
                           [CallerLineNumber] int sourceLineNumber = 0)
    {
        Console.WriteLine($"메시지: {message}");
        Console.WriteLine($"호출 멤버: {memberName}");
        Console.WriteLine($"소스 파일: {sourceFilePath}");
        Console.WriteLine($"줄 번호: {sourceLineNumber}");
        Console.WriteLine("---");
    }
}

public class CallerInfoExample
{
    public void DoSomething()
    {
        Logger.Log("작업 수행 중...");
    }

    public static void Main(string[] args)
    {
        CallerInfoExample example = new CallerInfoExample();
        example.DoSomething();

        Logger.Log("메인 메소드에서 직접 호출");
    }
}

내가 만드는 애트리뷰트 (Custom Attributes)

.NET의 내장 애트리뷰트 외에도, 개발자가 필요에 따라 직접 애트리뷰트를 정의하여 사용할 수 있습니다. 사용자 지정 애트리뷰트는 System.Attribute 클래스를 상속하여 만듭니다.

사용자 지정 애트리뷰트는 다음과 같이 정의됩니다.

  1. System.Attribute를 상속하는 새 클래스를 정의합니다. 관례적으로 클래스 이름은 Attribute로 끝납니다 (예: MyCustomAttribute). 사용할 때는 Attribute 접미사를 생략할 수 있습니다 (예: [MyCustom(...)]).
  2. [AttributeUsage] 애트리뷰트를 사용하여 사용자 지정 애트리뷰트를 적용할 수 있는 코드 요소(클래스, 메소드, 속성 등)와 적용 규칙(여러 번 적용 가능 여부, 상속 여부 등)을 지정합니다.
  3. 애트리뷰트에 필요한 데이터를 저장할 public 속성(Property) 또는 생성자 매개변수를 정의합니다.
using System;
using System.Reflection;

// 1. 사용자 지정 애트리뷰트 정의
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, // 클래스와 메소드에 적용 가능
                AllowMultiple = true, // 여러 번 적용 가능
                Inherited = false)] // 상속되지 않음
public class HistoryAttribute : Attribute
{
    public string Author { get; } // 읽기 전용 속성
    public double Version { get; set; } // 읽기/쓰기 속성
    public string Description { get; set; }

    // 생성자
    public HistoryAttribute(string author)
    {
        Author = author;
        Version = 1.0; // 기본값 설정
    }
}

// 2. 사용자 지정 애트리뷰트 사용
[History("Alice", Version = 1.0, Description = "초기 클래스 생성")]
[History("Bob", Version = 1.1, Description = "기능 추가")]
public class MyAppComponent
{
    [History("Alice", Version = 1.0, Description = "메소드 생성")]
    public void PerformAction()
    {
        Console.WriteLine("작업 수행!");
    }
}

// 3. 리플렉션을 이용해 사용자 지정 애트리뷰트 정보 읽기
public class CustomAttributeReader
{
    public static void Main(string[] args)
    {
        Type type = typeof(MyAppComponent);

        // 클래스에 적용된 History 애트리뷰트 읽기
        Console.WriteLine("--- MyAppComponent 클래스 History ---");
        Attribute[] classAttrs = Attribute.GetCustomAttributes(type, typeof(HistoryAttribute));
        foreach (Attribute attr in classAttrs)
        {
            if (attr is HistoryAttribute history)
            {
                Console.WriteLine($"작성자: {history.Author}, 버전: {history.Version}, 설명: {history.Description}");
            }
        }

        // PerformAction 메소드에 적용된 History 애트리뷰트 읽기
        Console.WriteLine("\n--- PerformAction 메소드 History ---");
        MethodInfo method = type.GetMethod("PerformAction");
        Attribute[] methodAttrs = Attribute.GetCustomAttributes(method, typeof(HistoryAttribute));
        foreach (Attribute attr in methodAttrs)
        {
            if (attr is HistoryAttribute history)
            {
                Console.WriteLine($"작성자: {history.Author}, 버전: {history.Version}, 설명: {history.Description}");
            }
        }
    }
}