Reflection은 주로 메타데이터를 읽고 멤버를 호출하는 용도로 설계되었습니다. 이미 로드된 타입의 메타데이터를 순수 Reflection으로 직접 수정할 수는 없습니다. 그럼에도 불구하고, 실무에서 “수정처럼” 보이게 다루는 방법은 있습니다. 이 글에서는 가능한 3가지 실용 루트를 간단히 정리합니다.
1. 현실과 한계
핵심 요약입니다.
1) 로드된 기존 타입의 특성(Attribute)이나 시그니처를 Reflection으로 교체/추가/삭제할 수 없습니다.
2) 대신 다음을 활용할 수 있습니다.
- TypeDescriptor: 디자인 타임/컴포넌트 모델 관점의 메타데이터를 런타임에 덧입히기
- Reflection.Emit: 새 동적 타입을 만들 때 원하는 메타데이터(특성 등) 정의
- 오프라인 리라이팅: Mono.Cecil/System.Reflection.Metadata로 디스크의 어셈블리를 수정 후 다시 로드
2. TypeDescriptor로 런타임에 메타데이터 덧입히기
WinForms, ComponentModel, 일부 도구(예: PropertyGrid)는 System.ComponentModel.TypeDescriptor를 통해 메타데이터를 조회합니다. 이를 이용하면 “특성을 추가한 것처럼” 동작시킬 수 있습니다. CLR 메타데이터 자체가 바뀌는 것은 아니지만, 해당 생태계에서는 수정 효과가 납니다.
using System;
using System.ComponentModel;
public class Product
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
// 적용 전
var before = TypeDescriptor.GetAttributes(typeof(Product));
var beforeDisplay = before[typeof(DisplayNameAttribute)] as DisplayNameAttribute;
Console.WriteLine($"Before: {beforeDisplay?.DisplayName ?? "(없음)"}");
// 런타임에 DisplayName 특성 덧입히기
TypeDescriptor.AddAttributes(
typeof(Product),
new DisplayNameAttribute("상품")
);
TypeDescriptor.Refresh(typeof(Product)); // 캐시 갱신 권장
// 적용 후
var after = TypeDescriptor.GetAttributes(typeof(Product));
var afterDisplay = after[typeof(DisplayNameAttribute)] as DisplayNameAttribute;
Console.WriteLine($"After: {afterDisplay?.DisplayName}");
}
}
주의: TypeDescriptor는 주로 디자인 타임/컴포넌트 소비자에게만 영향을 줍니다. 일반 Reflection(Attribute.GetCustomAttribute 등)이나 대부분의 직렬화기는 이를 인식하지 않습니다.
3. Reflection.Emit으로 새 타입 생성 시 메타데이터 정의
기존 타입은 못 바꾸지만, 새 동적 타입을 만들 때는 특성을 자유롭게 부여할 수 있습니다. 아래 예시는 타입에 Obsolete 특성, 속성에 DisplayName 특성을 부여합니다.
using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
class Program
{
static void Main()
{
var asmName = new AssemblyName("DynAsm");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var module = asm.DefineDynamicModule("Main");
var tb = module.DefineType("DynamicProduct", TypeAttributes.Public | TypeAttributes.Class);
// [Obsolete("...")] 타입 특성 부여
var obCtor = typeof(ObsoleteAttribute).GetConstructor(new[] { typeof(string) });
var obAttr = new CustomAttributeBuilder(obCtor, new object[] { "이 타입은 예제입니다" });
tb.SetCustomAttribute(obAttr);
// string Name { get; set; } + [DisplayName("상품명")] 속성 구성
var fb = tb.DefineField("_name", typeof(string), FieldAttributes.Private);
var pb = tb.DefineProperty("Name", PropertyAttributes.None, typeof(string), null);
var getM = tb.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(string), Type.EmptyTypes);
var ilGet = getM.GetILGenerator();
ilGet.Emit(OpCodes.Ldarg_0);
ilGet.Emit(OpCodes.Ldfld, fb);
ilGet.Emit(OpCodes.Ret);
var setM = tb.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { typeof(string) });
var ilSet = setM.GetILGenerator();
ilSet.Emit(OpCodes.Ldarg_0);
ilSet.Emit(OpCodes.Ldarg_1);
ilSet.Emit(OpCodes.Stfld, fb);
ilSet.Emit(OpCodes.Ret);
pb.SetGetMethod(getM);
pb.SetSetMethod(setM);
var dnCtor = typeof(DisplayNameAttribute).GetConstructor(new[] { typeof(string) });
var dnAttr = new CustomAttributeBuilder(dnCtor, new object[] { "상품명" });
pb.SetCustomAttribute(dnAttr);
var t = tb.CreateType();
var typeAttr = t.GetCustomAttribute<ObsoleteAttribute>();
Console.WriteLine("Type Obsolete: " + typeAttr?.Message);
var disp = t.GetProperty("Name").GetCustomAttribute<DisplayNameAttribute>();
Console.WriteLine("Property DisplayName: " + disp?.DisplayName);
}
}
참고: .NET Core/5+의 Reflection.Emit은 대부분 메모리 내 실행 전용입니다. 디스크로 저장은 제한됩니다.
4. 오프라인으로 기존 어셈블리 메타데이터 수정 (Mono.Cecil)
이미 빌드된 DLL의 메타데이터를 “파일 수준”에서 편집하려면 Mono.Cecil이 가장 간단합니다. 아래는 Product 타입에 [Serializable]을 주입하고 새 DLL로 저장하는 예시입니다.
// NuGet: Mono.Cecil
using System;
using System.Linq;
using Mono.Cecil;
class Patch
{
static void Main()
{
var asm = AssemblyDefinition.ReadAssembly("Target.dll");
var module = asm.MainModule;
var type = module.Types.First(t => t.Name == "Product");
var ctorRef = module.ImportReference(typeof(SerializableAttribute).GetConstructor(Type.EmptyTypes));
var ca = new CustomAttribute(ctorRef);
type.CustomAttributes.Add(ca);
asm.Write("Target.Patched.dll");
Console.WriteLine("Patched.");
}
}
주의: 오프라인 리라이팅은 강력하지만 IL/메타데이터 호환성, 강명명(Strong Name) 재서명, 보안 정책 등을 신중히 다뤄야 합니다. 테스트와 검증이 필수입니다.
5. 무엇을 언제 써야 할까?
- UI/디자인 타임 메타데이터만 바꾸면 충분: TypeDescriptor (간단, 위험도 낮음)
- 런타임에 새로운 타입/프록시가 필요: Reflection.Emit 또는 Castle DynamicProxy 같은 프록시 프레임워크
- 기존 DLL 자체를 바꿔야 함: Mono.Cecil 또는 System.Reflection.Metadata로 오프라인 패치
- ASP.NET Core MVC의 모델 메타데이터 커스터마이징: IDisplayMetadataProvider/ IValidationMetadataProvider 구현으로 프레임워크 레벨에서 처리(실제 CLR 메타데이터 변경 아님)
정리: 순수 Reflection만으로는 “기존 타입 메타데이터 수정”은 불가합니다. 대신 목적에 맞는 우회 경로를 선택하면 실무 요구(표시 이름 변경, 특성 기반 동작 제어, 규칙 주입 등)를 안전하고 예측 가능하게 달성할 수 있습니다.
'C#' 카테고리의 다른 글
| C# Nullable 연산자(??, ??=, ?.) 심층 이해 (0) | 2026.06.12 |
|---|---|
| C# 프로퍼티 패턴(Property Patterns) 활용 예제 (0) | 2026.06.12 |
| C# Operator Overloading으로 사용자 정의 연산 구현 (0) | 2026.06.11 |
| C# 메서드 오버로딩과 오버라이딩 차이 및 모범 사례 (0) | 2026.06.10 |
| C# 인터페이스의 명시적 구현 활용과 주의사항 (0) | 2026.06.05 |