본문으로 건너뛰기

표현식 트리 기초

Part 1에서 만든 Specification은 IsSatisfiedBy 메서드로 메모리 컬렉션을 필터링합니다. 하지만 실제 애플리케이션에서는 데이터가 데이터베이스에 있습니다. EF Core 같은 ORM은 C# 람다를 SQL로 변환하는데, 이를 위해서는 bool을 반환하는 메서드가 아니라 Expression Tree가 필요합니다. 이 장에서는 Expression Tree가 무엇이고, 왜 Specification에 필요한지 알아봅니다.

Func는 실행 가능한 블랙박스이고, Expression은 검사 가능한 트리 구조입니다.

  1. Func vs Expression의 근본적 차이를 설명할 수 있습니다

    • Func<T, bool>은 컴파일된 델리게이트로 내부 구조를 알 수 없음
    • Expression<Func<T, bool>>은 코드의 구조를 트리 형태로 보존함
    • Expression은 Body, Parameters, NodeType 등을 통해 검사 가능
  2. Expression Tree가 ORM에 필요한 이유를 설명할 수 있습니다

    • EF Core는 LINQ 쿼리를 SQL로 번역해야 함
    • Func는 불투명하여 SQL로 변환할 수 없음
    • Expression은 트리를 순회하며 SQL 절로 변환 가능
  3. Expression을 컴파일하고 캐싱하여 메모리에서 실행할 수 있습니다

    • Expression.Compile()로 Func로 변환하여 메모리에서 실행 가능
    • 컴파일된 결과는 캐싱하여 재사용하는 것이 효율적
  • Expression의 Body, Parameters 속성 접근
  • Expression을 Compile하여 Func로 변환 후 실행
  • AsQueryable()에서 Expression 기반 Where 사용

Func<Product, bool>은 컴파일된 델리게이트입니다. 런타임에 호출하여 결과를 얻을 수 있지만, “어떤 조건인지”를 프로그래밍적으로 알아낼 수 없습니다.

Func<Product, bool> func = p => p.Price > 1000;
// func의 내부를 검사할 방법이 없음
// EF Core가 이것을 SQL로 번역할 수 없음

Expression: 검사 가능한 트리 구조

섹션 제목: “Expression: 검사 가능한 트리 구조”

Expression<Func<Product, bool>>은 동일한 람다식을 트리 구조로 보존합니다. 컴파일러가 람다식을 코드가 아닌 데이터 구조로 변환합니다.

Expression<Func<Product, bool>> expr = p => p.Price > 1000;
// 트리 구조 검사 가능
Console.WriteLine(expr.Body); // (p.Price > 1000)
Console.WriteLine(expr.Parameters); // p
Console.WriteLine(expr.Body.NodeType); // GreaterThan

Expression은 Compile() 메서드를 통해 실행 가능한 Func로 변환할 수 있습니다. 이 과정은 비용이 있으므로 결과를 캐싱하는 것이 좋습니다.

var compiled = expr.Compile();
var result = compiled(product); // true/false

ORM이 Expression을 필요로 하는 이유

섹션 제목: “ORM이 Expression을 필요로 하는 이유”
구분FuncExpression
내부 구조불투명 (블랙박스)검사 가능 (트리)
SQL 변환불가능가능
IQueryable지원 불가Where 절에 사용 가능
실행 위치항상 메모리DB 서버 또는 메모리
ExpressionIntro/ # 메인 프로젝트
├── Program.cs # Expression Tree 데모
├── Product.cs # 상품 레코드
├── ExpressionIntro.csproj # 프로젝트 파일
ExpressionIntro.Tests.Unit/ # 테스트 프로젝트
├── ExpressionBasicsTests.cs # Expression 기초 테스트
├── Using.cs # 글로벌 using
├── xunit.runner.json # xUnit 설정
├── ExpressionIntro.Tests.Unit.csproj # 테스트 프로젝트 파일
index.md # 이 문서
public record Product(string Name, decimal Price, int Stock, string Category);
// Func - 불투명한 블랙박스
Func<Product, bool> func = p => p.Price > 1000;
// Expression - 검사 가능한 트리
Expression<Func<Product, bool>> expr = p => p.Price > 1000;
Console.WriteLine($"Body: {expr.Body}");
Console.WriteLine($"Parameters: {string.Join(", ", expr.Parameters)}");
// Expression → Func 컴파일
var compiled = expr.Compile();
var product = new Product("노트북", 1_500_000, 10, "전자제품");
Console.WriteLine($"Result: {compiled(product)}");
구분Func<T, bool>Expression<Func<T, bool>>
본질컴파일된 코드코드의 데이터 표현
검사불가능Body, Parameters 등 접근 가능
SQL 변환불가능ORM이 트리를 순회하여 변환
실행직접 호출Compile() 후 호출
IEnumerableWhere에 사용 가능Compile 필요
IQueryable사용 불가Where에 직접 사용 가능
  1. Expression은 코드를 데이터로 표현한 것으로, 프로그래밍적으로 분석하고 변환할 수 있습니다.
  2. ORM은 Expression이 있어야 SQL을 생성할 수 있습니다. Func만으로는 전체 데이터를 메모리에 로드해야 합니다.
  3. Compile()은 비용이 있으므로 캐싱하는 것이 좋습니다.

Q1: Expression Tree는 어디에서 사용되나요?

섹션 제목: “Q1: Expression Tree는 어디에서 사용되나요?”

A: 주로 ORM(Entity Framework Core), LINQ to SQL, 동적 쿼리 빌더 등에서 사용됩니다. C# 코드로 작성한 조건식을 SQL이나 다른 쿼리 언어로 변환해야 할 때 Expression Tree가 필수입니다.

Q2: Func 대신 항상 Expression을 사용해야 하나요?

섹션 제목: “Q2: Func 대신 항상 Expression을 사용해야 하나요?”

A: 아닙니다. 메모리 내 컬렉션을 필터링할 때는 Func가 더 효율적입니다. Expression은 SQL 변환이 필요한 경우에만 사용하면 됩니다. Specification 패턴에서는 두 가지 시나리오를 모두 지원하기 위해 Expression 기반을 사용합니다.

Q3: Expression.Compile()의 성능 비용은 얼마나 되나요?

섹션 제목: “Q3: Expression.Compile()의 성능 비용은 얼마나 되나요?”

A: Compile()은 Expression Tree를 IL 코드로 변환하는 과정이므로 상대적으로 비용이 큽니다. 따라서 한 번 컴파일한 결과를 캐싱하여 재사용하는 것이 좋습니다. Functorium의 ExpressionSpecification은 내부적으로 이 캐싱을 자동으로 수행합니다.

A: AsQueryable()IEnumerable<T>IQueryable<T>로 변환합니다. IQueryable은 Expression 기반의 Where를 지원하므로, 메모리 내 컬렉션에서도 Expression 기반 필터링을 테스트할 수 있습니다. 실제 프로젝트에서는 EF Core의 DbSet<T>IQueryable<T>을 구현합니다.


Expression Tree의 개념을 이해했으니, 다음 장에서는 이를 Specification에 통합한 ExpressionSpecification<T> 클래스를 직접 구현합니다.

2장: ExpressionSpecification 클래스