C# Expression Tree 심화
Expression Tree란
섹션 제목: “Expression Tree란”Expression Tree는 코드를 데이터 구조(트리)로 표현합니다. 람다를 Func<T>가 아닌 Expression<Func<T>>로 받으면 코드를 분석·변환·원격 실행할 수 있습니다.
// Func<int, bool>: 즉시 실행 가능한 델리게이트Func<int, bool> func = x => x > 5;
// Expression<Func<int, bool>>: 분석 가능한 트리 구조Expression<Func<int, bool>> expr = x => x > 5;
// 트리 구조 출력Console.WriteLine(expr); // x => (x > 5)Console.WriteLine(expr.Body); // (x > 5)Console.WriteLine(expr.Body.NodeType); // GreaterThan트리 구조 탐색
섹션 제목: “트리 구조 탐색”Expression<Func<int, bool>> expr = x => x > 5 && x < 100;
var binary = (BinaryExpression)expr.Body; // AndAlsovar left = (BinaryExpression)binary.Left; // x > 5var right = (BinaryExpression)binary.Right; // x < 100
Console.WriteLine(left.Left); // x (ParameterExpression)Console.WriteLine(left.Right); // 5 (ConstantExpression)동적 Expression 빌드
섹션 제목: “동적 Expression 빌드”// x => x.Age > minAge && x.Name.Contains(keyword) 동적 생성static Expression<Func<User, bool>> BuildFilter(int? minAge, string? keyword) { var param = Expression.Parameter(typeof(User), "x"); Expression body = Expression.Constant(true);
if (minAge.HasValue) { var ageProp = Expression.Property(param, nameof(User.Age)); var ageConst = Expression.Constant(minAge.Value); var ageFilter = Expression.GreaterThan(ageProp, ageConst); body = Expression.AndAlso(body, ageFilter); }
if (!string.IsNullOrEmpty(keyword)) { var nameProp = Expression.Property(param, nameof(User.Name)); var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!; var nameFilter = Expression.Call(nameProp, containsMethod, Expression.Constant(keyword)); body = Expression.AndAlso(body, nameFilter); }
return Expression.Lambda<Func<User, bool>>(body, param);}
var filter = BuildFilter(minAge: 18, keyword: "Alice");var results = dbContext.Users.Where(filter).ToList();컴파일과 캐싱
섹션 제목: “컴파일과 캐싱”Expression을 컴파일하면 델리게이트가 됩니다. 컴파일 비용이 크므로 캐싱이 필수입니다.
private static readonly ConcurrentDictionary<string, Func<User, bool>> _cache = new();
public IEnumerable<User> Filter(int? minAge, string? keyword) { string key = $"{minAge}_{keyword}"; var compiled = _cache.GetOrAdd(key, _ => BuildFilter(minAge, keyword).Compile()); return _users.Where(compiled);}ExpressionVisitor로 트리 변환
섹션 제목: “ExpressionVisitor로 트리 변환”ExpressionVisitor를 상속하면 트리를 순회하며 특정 노드를 변환할 수 있습니다.
// 모든 문자열 비교를 대소문자 무시로 변환class CaseInsensitiveVisitor : ExpressionVisitor { protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.Name == nameof(string.Equals) && node.Method.DeclaringType == typeof(string)) { return Expression.Call( node.Object!, typeof(string).GetMethod(nameof(string.Equals), new[] { typeof(string), typeof(StringComparison) })!, node.Arguments[0], Expression.Constant(StringComparison.OrdinalIgnoreCase)); } return base.VisitMethodCall(node); }}
var visitor = new CaseInsensitiveVisitor();var modified = (Expression<Func<User, bool>>)visitor.Visit(originalExpr);LINQ Provider와 IQueryable
섹션 제목: “LINQ Provider와 IQueryable”EF Core 같은 ORM이 Expression Tree를 SQL로 변환하는 원리입니다.
// IQueryable<T>.Where()는 Expression을 저장만 함var query = dbContext.Users .Where(u => u.Age > 18) // Expression 추가 .OrderBy(u => u.Name) // Expression 추가 .Select(u => u.Name); // Expression 추가// 아직 DB 호출 없음
var result = query.ToList(); // 여기서 전체 트리를 SQL로 변환 후 실행Expression<Func<T>>는 코드를 데이터로 표현 → 분석·변환·SQL 변환 가능- 동적 필터 빌더: 조건을 런타임에 조합해
IQueryable에 전달 - 컴파일(
Compile())은 비용이 크므로 결과 캐싱 필수 ExpressionVisitor로 기존 트리를 변환해 동작 수정 가능- EF Core, Dapper 등 ORM의 LINQ 지원이 이 메커니즘을 사용함