콘텐츠로 이동

C# Reflection & Expression Tree

Reflection은 런타임에 타입 정보를 조회하고 메서드를 동적으로 호출하는 강력한 기능입니다. 그러나 매 호출마다 메타데이터 조회와 바인딩이 발생해 직접 호출보다 수십~수백 배 느립니다. Expression Tree는 Reflection의 유연성을 유지하면서 컴파일된 델리게이트로 성능을 회복하는 핵심 기법입니다.


using System.Reflection;
public class Player
{
public string Name { get; set; } = "";
private int _health = 100;
public void TakeDamage(int amount) => _health -= amount;
private void SecretMethod() => Console.WriteLine("secret");
}
// 타입 정보 획득
Type type = typeof(Player);
// 또는: Type type = player.GetType();
// 공개 속성 목록
foreach (PropertyInfo prop in type.GetProperties())
Console.WriteLine($"{prop.Name}: {prop.PropertyType.Name}");
// private 필드 접근 (BindingFlags 필요)
FieldInfo healthField = type.GetField("_health",
BindingFlags.NonPublic | BindingFlags.Instance)!;
var player = new Player();
int health = (int)healthField.GetValue(player)!;
healthField.SetValue(player, 50);

2. 동적 메서드 호출과 성능 비용

섹션 제목: “2. 동적 메서드 호출과 성능 비용”
var player = new Player();
MethodInfo method = typeof(Player).GetMethod("TakeDamage")!;
// 방법 1: MethodInfo.Invoke — 가장 느림 (박싱, 메타데이터 바인딩)
method.Invoke(player, new object[] { 10 });
// 방법 2: CreateDelegate — 델리게이트로 캐싱 후 재사용
var del = (Action<int>)Delegate.CreateDelegate(typeof(Action<int>), player, method);
del(10); // 직접 호출에 가까운 속도
// 성능 비교 (100만 회 호출 기준, 대략적):
// 직접 호출: ~2ms
// CreateDelegate: ~5ms
// MethodInfo.Invoke: ~300ms

3. Expression Tree — 런타임 코드 컴파일

섹션 제목: “3. Expression Tree — 런타임 코드 컴파일”

Expression Tree는 코드를 데이터 구조(트리)로 표현하고, Compile()로 IL을 생성해 실행합니다.

using System.Linq.Expressions;
// x => x * 2 + 1 을 Expression Tree로 구성
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Add(
Expression.Multiply(x, Expression.Constant(2)),
Expression.Constant(1)
);
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> compiled = lambda.Compile();
Console.WriteLine(compiled(5)); // 11

3.2 Reflection 대체 — 속성 getter 캐싱

섹션 제목: “3.2 Reflection 대체 — 속성 getter 캐싱”
using System.Linq.Expressions;
using System.Collections.Concurrent;
public static class FastPropertyAccessor
{
// 타입별 getter 캐시
private static readonly ConcurrentDictionary<(Type, string), Func<object, object?>>
_getterCache = new();
public static Func<object, object?> GetGetter(Type type, string propertyName)
{
return _getterCache.GetOrAdd((type, propertyName), key =>
{
var (t, name) = key;
PropertyInfo prop = t.GetProperty(name)
?? throw new ArgumentException($"Property '{name}' not found on {t.Name}");
// Expression Tree로 getter 컴파일
ParameterExpression objParam = Expression.Parameter(typeof(object), "obj");
Expression castExpr = Expression.Convert(objParam, t);
Expression accessExpr = Expression.Property(castExpr, prop);
Expression boxExpr = Expression.Convert(accessExpr, typeof(object));
return Expression.Lambda<Func<object, object?>>(boxExpr, objParam).Compile();
});
}
}
// 사용
var player = new Player { Name = "Alice" };
var getter = FastPropertyAccessor.GetGetter(typeof(Player), "Name");
// 첫 호출: Expression Tree 컴파일 (느림)
// 이후 호출: 캐시된 델리게이트 (빠름)
string name = (string)getter(player)!;

public static class ObjectFactory<T> where T : new()
{
// Activator.CreateInstance 대신 Expression Tree 사용
private static readonly Func<T> _factory = CreateFactory();
private static Func<T> CreateFactory()
{
NewExpression newExpr = Expression.New(typeof(T));
return Expression.Lambda<Func<T>>(newExpr).Compile();
}
public static T Create() => _factory();
}
// Activator.CreateInstance vs Expression Factory (100만 회):
// Activator.CreateInstance: ~180ms
// Expression Factory: ~15ms

public static Action<object, object?> GetSetter(Type type, string propertyName)
{
PropertyInfo prop = type.GetProperty(propertyName)!;
ParameterExpression objParam = Expression.Parameter(typeof(object), "obj");
ParameterExpression valParam = Expression.Parameter(typeof(object), "val");
Expression castObj = Expression.Convert(objParam, type);
Expression castVal = Expression.Convert(valParam, prop.PropertyType);
Expression setExpr = Expression.Assign(
Expression.Property(castObj, prop), castVal);
return Expression.Lambda<Action<object, object?>>(setExpr, objParam, valParam).Compile();
}

Reflection + Expression Tree를 결합한 DTO 자동 매핑 예시입니다.

[AttributeUsage(AttributeTargets.Property)]
public class MapFromAttribute(string sourceName) : Attribute
{
public string SourceName { get; } = sourceName;
}
public class UserDto
{
[MapFrom("user_name")]
public string? Name { get; set; }
[MapFrom("user_level")]
public int Level { get; set; }
}
public static class AutoMapper
{
public static TDest Map<TDest>(Dictionary<string, object> source) where TDest : new()
{
var dest = new TDest();
foreach (var prop in typeof(TDest).GetProperties())
{
var attr = prop.GetCustomAttribute<MapFromAttribute>();
string key = attr?.SourceName ?? prop.Name;
if (source.TryGetValue(key, out var value))
prop.SetValue(dest, Convert.ChangeType(value, prop.PropertyType));
}
return dest;
}
}
// 사용
var data = new Dictionary<string, object>
{
["user_name"] = "Alice",
["user_level"] = 10
};
UserDto dto = AutoMapper.Map<UserDto>(data);

  • MethodInfo.Invoke는 매 호출마다 메타데이터 바인딩과 박싱이 발생해 직접 호출보다 수십 배 느리다.
  • Delegate.CreateDelegate로 델리게이트를 캐싱하면 Reflection 오버헤드를 제거할 수 있다.
  • Expression Tree는 런타임에 코드 구조를 데이터로 표현하고 Compile()로 IL을 생성한다. 첫 컴파일 비용은 크지만 ConcurrentDictionary로 캐싱하면 이후 호출은 직접 호출에 가까운 속도가 된다.
  • 핫 경로에 Reflection이 꼭 필요하다면 Expression Tree + 캐싱 패턴을 사용하고, 콜드 경로(초기화, 설정 로딩)에서는 단순 Reflection도 허용된다.