본문 바로가기

C#

C# Reflection으로 메타데이터 수정하기

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만으로는 “기존 타입 메타데이터 수정”은 불가합니다. 대신 목적에 맞는 우회 경로를 선택하면 실무 요구(표시 이름 변경, 특성 기반 동작 제어, 규칙 주입 등)를 안전하고 예측 가능하게 달성할 수 있습니다.