콘텐츠로 이동

C# Expression Trees와 동적 코드 생성

Expression Trees는 코드를 데이터(트리 구조)로 표현하는 C#의 기능입니다. 람다를 실행하는 대신 분석하거나, 런타임에 새 코드를 조합하여 컴파일할 수 있습니다. LINQ to SQL, Entity Framework, 동적 쿼리 빌더 등이 이 기술을 기반으로 합니다.


// 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); // 실행

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")

// 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)); // True
Console.WriteLine(compiled(5)); // False
// item => item.Price > 100
var 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();

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();

// 타입 안전 멤버 이름 추출 (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"

// 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.99

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 구현의 핵심입니다.