C# Reflection & Expression Tree
Reflection은 런타임에 타입 정보를 조회하고 메서드를 동적으로 호출하는 강력한 기능입니다. 그러나 매 호출마다 메타데이터 조회와 바인딩이 발생해 직접 호출보다 수십~수백 배 느립니다. Expression Tree는 Reflection의 유연성을 유지하면서 컴파일된 델리게이트로 성능을 회복하는 핵심 기법입니다.
1. Reflection 기본
섹션 제목: “1. 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: ~300ms3. Expression Tree — 런타임 코드 컴파일
섹션 제목: “3. Expression Tree — 런타임 코드 컴파일”Expression Tree는 코드를 데이터 구조(트리)로 표현하고, Compile()로 IL을 생성해 실행합니다.
3.1 간단한 산술 표현식
섹션 제목: “3.1 간단한 산술 표현식”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)); // 113.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)!;4. 제네릭 팩토리 패턴
섹션 제목: “4. 제네릭 팩토리 패턴”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: ~15ms5. 속성 setter 구현
섹션 제목: “5. 속성 setter 구현”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();}6. 어트리뷰트 기반 자동 매핑
섹션 제목: “6. 어트리뷰트 기반 자동 매핑”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도 허용된다.