서론
C#에 대해 심화하여 학습하며 가장 미스테리하고 매력적으로 다가온 기능 중 하나는 리플렉션(Reflection)입니다. 리플렉션은 프로그램이 실행 중에 자신의 구조와 동작을 검사하고 수정할 수 있게 해주는 강력한 기능입니다. 마치 프로그램이 거울을 보며 자신을 이해하고 변화시킬 수 있는 능력을 갖게 되는 것입니다.
본론
1. 리플렉션의 기본 개념
리플렉션이라는 이름은 '반사'라는 뜻을 가진 단어에서 왔습니다. 프로그램이 마치 거울을 보듯 자신의 내부를 들여다본다는 의미를 담고 있습니다. 이 기능을 통해 우리는 실행 중인 프로그램의 타입 정보를 검사하고, 필요한 경우 동적으로 객체를 생성하거나 메서드를 호출할 수 있습니다.
1.1 Object.GetType()과 Type 클래스의 이해
.NET의 타입 시스템에서 모든 타입은 System.Object를 상속받습니다. 이는 C#의 핵심 설계 원칙 중 하나인데, 이를 통해 모든 객체가 공통으로 가져야 할 기본적인 동작들을 정의합니다. 그 중에서도 GetType() 메서드는 리플렉션의 시작점이 됩니다.
public class Example
{
public string Name { get; set; }
public void PrintName()
{
Console.WriteLine(Name);
}
}
Example obj = new Example();
Type type = obj.GetType(); // 객체의 Type 정보를 얻어옴
Console.WriteLine($"Type Name: {type.FullName}"); // 출력: Type Name: Example
C#
복사
Type 클래스는 .NET의 타입 시스템을 대표하는 클래스입니다. 이 클래스는 다음과 같은 중요한 정보들을 제공합니다:
•
타입의 이름과 네임스페이스
•
상속 계층 구조
•
구현된 인터페이스들
•
멤버(필드, 프로퍼티, 메서드, 이벤트) 정보
•
어셈블리 정보
1.2 Type 클래스 심화 탐구
Type 클래스를 통해 얻을 수 있는 정보들을 좀 더 자세히 살펴보겠습니다:
Type type = typeof(Example); // Type 정보를 얻는 또 다른 방법
// 프로퍼티 정보 조회
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
Console.WriteLine($"Property: {prop.Name}, Type: {prop.PropertyType}");
}
// 메서드 정보 조회
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"Method: {method.Name}");
}
C#
복사
여기서 주목할 점은 typeof 연산자와 GetType() 메서드의 차이입니다. typeof는 컴파일 타임에 타입 정보를 얻는 반면, GetType()은 런타임에 객체의 실제 타입 정보를 반환합니다. 이는 다형성을 활용할 때 특히 중요한 차이가 됩니다.
1.3 실제 활용 사례와 주의점
리플렉션은 강력한 기능이지만, 그만큼 신중하게 사용해야 합니다. 제가 처음 리플렉션을 배울 때는 그저 신기한 기능이라고만 생각했는데, 실제 프로젝트에서 사용해보니 몇 가지 중요한 점들을 발견했습니다:
1.
성능 고려사항:
•
리플렉션 연산은 일반적인 직접 참조보다 느립니다
•
성능이 중요한 코드에서는 결과를 캐싱하는 것이 좋습니다
2.
안전성:
•
런타임에 타입을 검사하므로 컴파일러의 타입 체크를 우회합니다
•
잘못된 사용은 런타임 오류를 발생시킬 수 있습니다
// 안전한 리플렉션 사용 예시
try
{
Type type = Type.GetType("Example");
if (type != null)
{
PropertyInfo prop = type.GetProperty("Name");
if (prop != null && prop.CanWrite)
{
// 프로퍼티 조작
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Reflection error: {ex.Message}");
}
C#
복사
2. 동적 객체 생성과 조작
리플렉션의 가장 강력한 기능 중 하나는 런타임에 동적으로 객체를 생성하고 조작할 수 있다는 점입니다. System.Activator 클래스가 이러한 작업의 중심이 됩니다.
2.1 동적 객체 생성
// 타입 이름으로부터 객체 생성
Type type = Type.GetType("MyNamespace.MyClass");
object instance = Activator.CreateInstance(type);
// 생성자 매개변수가 있는 경우
object instance2 = Activator.CreateInstance(type,
new object[] { "param1", 42 });
C#
복사
처음에는 이렇게 문자열로 타입을 지정하는 것이 불안하게 느껴졌습니다. 그러나 이러한 동적 생성 능력은 플러그인 아키텍처나 확장 가능한 애플리케이션을 만들 때 매우 유용하다는 것을 알게 되었습니다.
2.2 프로퍼티와 메서드 동적 호출
동적으로 생성된 객체의 프로퍼티와 메서드를 조작하는 방법을 살펴보겠습니다:
Type type = obj.GetType();
// 프로퍼티 값 설정
PropertyInfo propInfo = type.GetProperty("Name");
propInfo.SetValue(obj, "New Name", null);
// 메서드 호출
MethodInfo methodInfo = type.GetMethod("PrintName");
methodInfo.Invoke(obj, null); // 매개변수가 없는 메서드 호출
C#
복사
여기서 주의할 점은 SetValue와 Invoke 메서드가 예외를 던질 수 있다는 것입니다. 적절한 예외 처리는 필수입니다:
try
{
PropertyInfo propInfo = type.GetProperty("NonExistentProperty");
if (propInfo != null && propInfo.CanWrite)
{
propInfo.SetValue(obj, "value", null);
}
else
{
Console.WriteLine("Property not found or read-only");
}
}
catch (ArgumentException ex)
{
Console.WriteLine($"Invalid argument: {ex.Message}");
}
catch (TargetException ex)
{
Console.WriteLine($"Object does not match target type: {ex.Message}");
}
C#
복사
3. 형식 내보내기(Type Emission)
리플렉션의 가장 고급 기능 중 하나는 런타임에 새로운 타입을 동적으로 생성하는 것입니다. System.Reflection.Emit 네임스페이스의 클래스들이 이를 가능하게 합니다.
// 동적 어셈블리 생성
AssemblyName aName = new AssemblyName("DynamicAssembly");
AssemblyBuilder ab = AssemblyBuilder.DefineDynamicAssembly(
aName, AssemblyBuilderAccess.Run);
// 모듈 생성
ModuleBuilder mb = ab.DefineDynamicModule("DynamicModule");
// 클래스 생성
TypeBuilder tb = mb.DefineType("DynamicType",
TypeAttributes.Public);
// 프로퍼티 추가
PropertyBuilder pb = tb.DefineProperty("DynamicProperty",
PropertyAttributes.HasDefault,
typeof(string),
null);
C#
복사
이 기능은 처음 접했을 때 굉장히 어렵게 느껴졌습니다. IL(Intermediate Language) 코드를 직접 다뤄야 하는 경우도 있기 때문입니다. 하지만 이러한 동적 타입 생성은 다음과 같은 상황에서 매우 유용합니다:
•
런타임에 필요한 데이터 구조 생성
•
동적 프록시 구현
•
코드 생성 도구 개발
결론
리플렉션은 C#의 가장 강력한 기능 중 하나이지만, 그만큼 신중하게 사용해야 합니다. 제가 리플렉션을 공부하면서 깨달은 중요한 점들을 공유하고 싶습니다:
1.
성능 고려:
•
리플렉션 연산은 일반적인 정적 코드보다 느립니다
•
자주 사용되는 정보는 캐싱하는 것이 좋습니다
2.
안전성:
•
런타임 오류 가능성을 항상 고려해야 합니다
•
적절한 예외 처리가 필수적입니다
3.
사용 시기:
•
플러그인 아키텍처 구현
•
제네릭 프레임워크 개발
•
단위 테스트 도구 제작
다음 포스트에서는 애트리뷰트(Attribute)에 대해 다루면서, 리플렉션과 애트리뷰트가 어떻게 시너지를 발휘하는지 살펴보도록 하겠습니다.
참고 자료
•
박상현, "이것이 C#이다", 한빛미디어, 2023