본문 바로가기

C#

C# Reflection 성능 최적화 - Expression Tree와 IL Emit

Reflection은 강력하지만, 잘못 사용하면 성능을 급격히 저하시킨다. 실무에서 Reflection을 사용하는 대부분의 프레임워크는 그대로 호출하지 않는다.

대신 다음과 같은 기법을 사용한다.

  • 메타데이터 캐싱
  • Expression Tree 컴파일
  • IL Emit (동적 메서드 생성)

1️⃣ Reflection이 느린 이유

Reflection 호출은 다음 단계를 거친다.

  1. 메타데이터 탐색
  2. 접근 제어 검사
  3. 런타임 바인딩
  4. Boxing / Unboxing

즉, 일반 메서드 호출 대비 수십 배 느릴 수 있다.

Reflection 직접 호출 예제


MethodInfo method = typeof(User).GetMethod("GetName");
string name = (string)method.Invoke(user, null);

이 코드는 단순하지만, 반복 호출 시 성능 병목이 된다.

📘 Microsoft 공식 문서: Reflection 개요

 

.NET의 리플렉션

.NET에서 리플렉션을 검토합니다. 로드된 어셈블리 및 클래스, 인터페이스, 구조체 및 열거형과 같이 그 안에 정의된 형식에 대한 정보를 가져옵니다.

learn.microsoft.com

 


2️⃣ 기본 최적화: 캐싱

가장 기본적인 최적화는 Reflection 결과 캐싱이다.


static Dictionary<string, MethodInfo> _cache = new();

MethodInfo GetMethod(Type type, string name)
{
    string key = type.FullName + name;
    if (!_cache.TryGetValue(key, out var method))
    {
        method = type.GetMethod(name);
        _cache[key] = method;
    }
    return method;
}

하지만 이것만으로는 충분하지 않다. Invoke 자체가 여전히 느리다.


3️⃣ Expression Tree란?

Expression Tree는 코드를 데이터 구조로 표현한 것이다.

이것을 컴파일하면, 런타임에 일반 메서드 호출과 거의 동일한 성능을 얻을 수 있다.

📘 Microsoft 공식 문서: Expression Tree 개요

 

식 트리 - C#

표현식 트리에 대해 알아봅니다. 각 노드가 식인 이러한 데이터 구조로 표현되는 코드를 컴파일하고 실행하는 방법을 알아보세요.

learn.microsoft.com

 

Expression Tree로 Method 호출 래핑


using System.Linq.Expressions;
using System.Reflection;

static Func<object, object> CreateGetter(PropertyInfo property)
{
    var instance = Expression.Parameter(typeof(object), "instance");
    var castInstance = Expression.Convert(instance, property.DeclaringType);
    var propertyAccess = Expression.Property(castInstance, property);
    var castResult = Expression.Convert(propertyAccess, typeof(object));

    return Expression
        .Lambda<Func<object, object>>(castResult, instance)
        .Compile();
}

이렇게 생성된 Delegate는 Invoke 대신 직접 호출된다.

사용 예


PropertyInfo prop = typeof(User).GetProperty("Name");
var getter = CreateGetter(prop);

string name = (string)getter(user);

Reflection Invoke 대비 10배 이상 빠른 성능을 보인다.


4️⃣ 실무 패턴: Mapper 구현

AutoMapper 같은 라이브러리는 내부적으로 Expression Tree를 활용한다.


foreach (var prop in sourceType.GetProperties())
{
    var targetProp = targetType.GetProperty(prop.Name);
    if (targetProp == null) continue;

    var getter = CreateGetter(prop);
    var setter = CreateSetter(targetProp);

    // 캐시에 저장
}

이 구조는 Reflection을 단 1회만 사용하고, 이후는 Delegate 호출로 처리한다.


5️⃣ IL Emit이란?

IL Emit은 중간 언어(IL)를 직접 생성하는 방식이다.

Expression Tree보다 더 빠르지만,

  • 가독성 낮음
  • 유지보수 어려움
  • 실수 시 런타임 크래시

때문에 정말 필요한 경우에만 사용한다.

📘 Microsoft 공식 문서: System.Reflection.Emit

 

System.Reflection.Emit 네임스페이스

컴파일러 또는 도구가 메타데이터 및 MSIL(Microsoft 중간 언어)을 내보내고 필요에 따라 디스크에 PE 파일을 생성할 수 있도록 하는 클래스를 포함합니다. 이러한 클래스의 기본 클라이언트는 스크

learn.microsoft.com

 

DynamicMethod 예제


DynamicMethod dm = new DynamicMethod(
    "GetName",
    typeof(string),
    new[] { typeof(object) }
);

ILGenerator il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, typeof(User));
il.Emit(OpCodes.Callvirt, typeof(User).GetProperty("Name").GetGetMethod());
il.Emit(OpCodes.Ret);

var func = (Func<object, string>)dm.CreateDelegate(typeof(Func<object, string>));

이 방식은 거의 네이티브 호출에 가까운 성능을 낸다.


6️⃣ Expression Tree vs IL Emit

구분 Expression Tree IL Emit
난이도
가독성 좋음 매우 나쁨
성능 매우 좋음 최상
추천 용도 대부분의 실무 프레임워크 핵심부

7️⃣ 실무 주의사항

  • Reflection 결과는 반드시 캐싱
  • Expression Compile 비용도 캐싱
  • Generic Delegate 활용
  • IL Emit은 테스트 필수

8️⃣ 마무리

Reflection은 느리다는 인식은 반은 맞고 반은 틀리다.

제대로 최적화된 Reflection은 매우 빠르다.

이 기법들을 이해하면?

  • DI 컨테이너 내부 이해
  • ORM 구조 파악
  • 고급 면접 질문 대응

까지 자연스럽게 연결된다.

반응형