C# Expression Trees와 동적 코드 생성
Expression Trees는 코드를 데이터(트리 구조)로 표현하는 C#의 기능입니다. 람다를 실행하는 대신 분석하거나, 런타임에 새 코드를 조합하여 컴파일할 수 있습니다. LINQ to SQL, Entity Framework, 동적 쿼리 빌더 등이 이 기술을 기반으로 합니다.
1. Expression<T> vs Func<T>
섹션 제목: “1. Expression<T> vs Func<T>”// Func<T>: 즉시 실행 가능한 델리게이트Func<int, bool> func = x => x > 5;bool result = func(10); // 실행
// Expression<Func<T>>: 코드를 데이터로 표현 (분석 가능)Expression<Func<int, bool>> expr = x => x > 5;// expr.Body: BinaryExpression (>)// expr.Parameters[0]: ParameterExpression (x)
// Expression → 컴파일 후 실행Func<int, bool> compiled = expr.Compile();bool result2 = compiled(10); // 실행2. Expression Tree 구조 탐색
섹션 제목: “2. Expression Tree 구조 탐색”Expression<Func<string, bool>> expr = s => s.Length > 3 && s.StartsWith("A");
// 트리 구조 출력void PrintTree(Expression node, int depth = 0){ string indent = new string(' ', depth * 2); Console.WriteLine($"{indent}{node.NodeType}: {node}");
if (node is BinaryExpression binary) { PrintTree(binary.Left, depth + 1); PrintTree(binary.Right, depth + 1); } else if (node is MethodCallExpression call) { PrintTree(call.Object!, depth + 1); foreach (var arg in call.Arguments) PrintTree(arg, depth + 1); }}
PrintTree(expr.Body);// AndAlso: (s.Length > 3) AndAlso s.StartsWith("A")// GreaterThan: s.Length > 3// MemberAccess: s.Length// Constant: 3// Call: s.StartsWith("A")3. 동적 Expression 생성
섹션 제목: “3. 동적 Expression 생성”// x => x > threshold를 런타임에 생성int threshold = 10;var param = Expression.Parameter(typeof(int), "x");var body = Expression.GreaterThan(param, Expression.Constant(threshold));var lambda = Expression.Lambda<Func<int, bool>>(body, param);var compiled = lambda.Compile();
Console.WriteLine(compiled(15)); // TrueConsole.WriteLine(compiled(5)); // False3.1 속성 접근
섹션 제목: “3.1 속성 접근”// item => item.Price > 100var param = Expression.Parameter(typeof(Product), "item");var property = Expression.Property(param, nameof(Product.Price));var constant = Expression.Constant(100m);var body = Expression.GreaterThan(property, constant);var lambda = Expression.Lambda<Func<Product, bool>>(body, param);
var filter = lambda.Compile();var expensive = products.Where(filter).ToList();4. 동적 LINQ 필터 빌더
섹션 제목: “4. 동적 LINQ 필터 빌더”public static class FilterBuilder{ public static Expression<Func<T, bool>> Build<T>( string propertyName, object value) { var param = Expression.Parameter(typeof(T), "x"); var property = Expression.Property(param, propertyName); var constant = Expression.Constant( Convert.ChangeType(value, property.Type)); var equality = Expression.Equal(property, constant); return Expression.Lambda<Func<T, bool>>(equality, param); }
public static Expression<Func<T, bool>> And<T>( this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right) { var param = left.Parameters[0]; var rightBody = new ParameterReplacer(right.Parameters[0], param) .Visit(right.Body); var body = Expression.AndAlso(left.Body, rightBody); return Expression.Lambda<Func<T, bool>>(body, param); }}
// 매개변수 치환기class ParameterReplacer : ExpressionVisitor{ private readonly ParameterExpression _old, _new; public ParameterReplacer(ParameterExpression old, ParameterExpression @new) => (_old, _new) = (old, @new); protected override Expression VisitParameter(ParameterExpression node) => node == _old ? _new : base.VisitParameter(node);}
// 사용var filter = FilterBuilder.Build<Product>("Category", "Electronics") .And(FilterBuilder.Build<Product>("InStock", true));
var results = products.Where(filter.Compile()).ToList();// EF Core에서는 직접 Where에 전달 (DB 쿼리로 변환)var dbResults = await dbContext.Products.Where(filter).ToListAsync();5. Expression으로 멤버 이름 추출
섹션 제목: “5. Expression으로 멤버 이름 추출”// 타입 안전 멤버 이름 추출 (nameof 대안)public static string GetMemberName<T, TProp>( Expression<Func<T, TProp>> expr){ return expr.Body switch { MemberExpression member => member.Member.Name, UnaryExpression { Operand: MemberExpression m } => m.Member.Name, _ => throw new ArgumentException("멤버 표현식이 아닙니다") };}
string name = GetMemberName<Product, decimal>(p => p.Price); // "Price"6. 고성능 속성 접근기 생성
섹션 제목: “6. 고성능 속성 접근기 생성”// Reflection보다 빠른 동적 속성 접근기public static Func<TObj, TProp> CreateGetter<TObj, TProp>(string propertyName){ var param = Expression.Parameter(typeof(TObj), "obj"); var property = Expression.Property(param, propertyName); var cast = Expression.Convert(property, typeof(TProp)); return Expression.Lambda<Func<TObj, TProp>>(cast, param).Compile();}
public static Action<TObj, TProp> CreateSetter<TObj, TProp>(string propertyName){ var obj = Expression.Parameter(typeof(TObj), "obj"); var value = Expression.Parameter(typeof(TProp), "value"); var property = Expression.Property(obj, propertyName); var assign = Expression.Assign(property, value); return Expression.Lambda<Action<TObj, TProp>>(assign, obj, value).Compile();}
// 캐싱하여 재사용var priceGetter = CreateGetter<Product, decimal>("Price");var priceSetter = CreateSetter<Product, decimal>("Price");
var product = new Product();priceSetter(product, 99.99m);decimal price = priceGetter(product); // 99.997. 실전 — 간단한 ORM 필터
섹션 제목: “7. 실전 — 간단한 ORM 필터”public class QueryBuilder<T>{ private readonly List<Expression<Func<T, bool>>> _filters = new();
public QueryBuilder<T> Where(Expression<Func<T, bool>> filter) { _filters.Add(filter); return this; }
public IQueryable<T> Apply(IQueryable<T> query) { return _filters.Aggregate(query, (q, f) => q.Where(f)); }}
// 사용var builder = new QueryBuilder<Product>() .Where(p => p.Category == "Electronics") .Where(p => p.Price < 500m) .Where(p => p.InStock);
var results = builder.Apply(dbContext.Products).ToList();// EF Core가 Expression을 SQL WHERE 절로 변환| 기술 | 용도 |
|---|---|
Expression<Func<T>> | 코드를 데이터로 표현 |
.Compile() | Expression을 실행 가능한 델리게이트로 변환 |
ExpressionVisitor | 트리 순회/변환 |
| 동적 Expression | 런타임 쿼리 조합 |
| 타입 안전 멤버 추출 | 컴파일 타임 속성 이름 검증 |
Expression Trees는 ORM, 동적 필터, 고성능 속성 접근, LINQ provider 구현의 핵심입니다.