Reflection
Reflection
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 클래스를 상속하여 만듭니다.
사용자 지정 애트리뷰트는 다음과 같이 정의됩니다.
System.Attribute를 상속하는 새 클래스를 정의합니다. 관례적으로 클래스 이름은Attribute로 끝납니다 (예:MyCustomAttribute). 사용할 때는Attribute접미사를 생략할 수 있습니다 (예:[MyCustom(...)]).[AttributeUsage]애트리뷰트를 사용하여 사용자 지정 애트리뷰트를 적용할 수 있는 코드 요소(클래스, 메소드, 속성 등)와 적용 규칙(여러 번 적용 가능 여부, 상속 여부 등)을 지정합니다.- 애트리뷰트에 필요한 데이터를 저장할 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}");
}
}
}
}