[로봇활용_11주차] C# 식 트리(Expression Tree)

최윤호·2025년 10월 27일
post-thumbnail

코드를 데이터처럼 다루는 기술

우리가 작성하는 코드는 본질적으로 컴퓨터에 내리는 '지시사항'들의 묶음입니다.
컴파일러는 이 지시사항을 컴퓨터가 이해할 수 있는 언어로 번역하고,
프로그램은 그에 따라 충실히 동작합니다.

여기서 이 '지시사항' 자체를 하나의 데이터처럼 다룰 수 있다면 어떨까요?
코드를 실행하기 전에 그 구조를 분석하고, 특정 부분을 다른 내용으로 바꾸거나,
심지어는 상황에 따라 완전히 새로운 코드를 동적으로 만들어낼 수 있다면 말입니다.

바로 이 마법 같은 일을 가능하게 해주는 기술이 C#의 식 트리(Expression Tree)입니다.
이번 글에서는 어렵지만 이 매력적인 개념을 최대한 쉽게 풀어보겠습니다!

1)식 트리란 무엇일까요?

식 트리의 세계로 들어가기 전, 가장 중요한 핵심을 먼저 짚고 넘어가겠습니다.
식 트리(Expression Tree)는 단순히 실행할 수 있는 코드가 아닙니다.
그것은 코드의 구조를 분석하고 조작할 수 있도록 설계된 '데이터 구조'입니다.
이 점을 이해하는 것이 식 트리를 제대로 활용하는 첫걸음입니다.

// 1. 대리자(Delegate)에 할당: 실행 가능한 '코드' (요리)
// 컴파일러는 이 람다 표현식를 즉시 실행 가능한 IL 코드로 번역합니다.
using System.Linq.Expressions;

Func<int, bool> isPositiveFunc = number => number > 0;

// 2. 식 트리(Expression Tree)에 할당: 분석 가능한 '데이터' (레시피)
// 컴파일러는 이 람다 표현식를 코드의 '구조'를 나타내는 데이터 객체로 만듭니다.
Expression<Func<int, bool>> isPositiveExpression = number => number > 0;

// --- 사용법의 차이 ---
bool result1 = isPositiveFunc(10); // OK! '요리'는 바로 호출해서 실행 가능
// bool result2 = isPositiveExpression(10); // 컴파일 오류! '레시피'는 실행 불가능한 코드

isPositiveFunc는 그 자체로 '기능'이지만,
isPositiveExpression은 코드의 구조를 설명하는 '데이터'일 뿐입니다.

2)식 트리의 내부 구조

식 트리는 코드를 트리(Tree) 형태의 데이터 구조로 표현한 것입니다.
트리(Tree)의 각 노드(Node)는 코드의 구성 요소를 나타내며, 컴파일된
실행 코드가 아니라, 코드가 어떤 구조로 이루어져 있는지를 정의하는 데이터입니다.
우선 간단한 코드를 예로 들어보겠습니다. var sum = 1 + 2; 이 코드를 식 트리로 표현하면,
컴파일러가 코드를 분석하는 방식과 유사하게 다음과 같은 정교한 구조를 갖게 됩니다.


  • 최상위 노드 (루트): 변수 선언 및 할당문 (var sum = 1 + 2;)
    • 변수 선언 (var sum)
      • 암시적 타입 키워드 (var)
      • 변수 이름 (sum)
    • 할당 연산자 (=)
    • 이항 덧셈 식 (1 + 2)
      • 왼쪽 피연산자: (상수 1)
      • 연산자: (덧셈 기호 +)
      • 오른쪽 피연산자: (상수 2)

이렇게 코드를 트리 구조의 데이터로 분해하면, 우리는 각 노드를 순회하며
'이 코드는 덧셈 연산을 하고, 그 대상은 상수12구나'와 같이
코드의 구조와 의도를 프로그램적으로 분석할 수 있게 됩니다.

3)식 트리는 어디에 사용될까요?

식트리의 가장 대표적인 예가 바로 LINQ to SQL (Entity Framework)입니다.
데이터베이스에서 데이터를 조회하기 위해 이런 C# 코드를 작성했다고 상상해 봅시다.

[C# 코드]

var query = context.Users.Where(u => u.Age > 20);

C# 컴파일러는 u => u.Age > 20부분을 단순한 코드가 아닌
식 트리로 변환하여 Entity Framework에 전달합니다.
Entity Framework는 이 식 트리를 분석하여,
"Users테이블에서 Age20보다 큰 데이터 찾기"라는 의도를 파악하고,
이를 실제 데이터베이스가 이해할 수 있는 SQL 쿼리로 번역합니다.

[SQL 쿼리]

SELECT * FROM Users WHERE Age > 20

만약 식 트리가 없었다면, C# 코드를 다른 언어(SQL)로 번역하는
이런 작업은 거의 불가능에 가까웠을 겁니다. 이처럼 식 트리는
코드 자체를 분석하고 다른 형태로 변환하는 강력한 기능을 제공합니다.

4)식 트리를 만드는 방법

C#에서는 크게 두 가지 방법을 제공합니다. 하나는 C# 컴파일러의 도움을
받아 간편하게 만드는 방법이고, 다른 하나는 Expression API를
사용하여 런타임에 동적으로 직접 조립하는 정교한 방법입니다.

1. 컴파일러에 맡기기

가장 일반적이고 간편한 방법은 람다식(Lambda Expression)을
Expression<TDelegate>타입의 변수에 할당하는 것입니다.
이렇게 하면 C# 컴파일러가 알아서 식 트리 구조를 자동으로 생성해 줍니다.

// "num < 5" 라는 조건을 표현하는 식 트리를 생성합니다.
Expression<Func<int, bool>> lambda = num => num < 5;

[주의]
이 방법은 본문이 단일 식으로 이루어진 '람다식(Expression lambda)'에만 적용됩니다.
{ }를 사용하여 본문이 여러 문장으로 이루어진 '람다문(Statement lambda)'
컴파일러가 식 트리(Expression Tree)로 변환할 수 없습니다.

2. API로 직접 조립

컴파일러의 도움 없이 런타임에 동적으로 코드를 구성해야 할 때가 있습니다.
이럴 때는 System.Linq.Expressions.Expression클래스가 제공하는
정적 팩토리 메서드를 사용하여 식 트리를 수동으로 조립할 수 있습니다.
여기서 기억해야 할 핵심 원칙은 식 트리는 불변(Immutable)한다는 것입니다.
즉, 한번 만들어진 노드는 변경할 수 없습니다. 따라서 식 트리를 만들 때는
가장 아래쪽의 잎(Leaf) 노드부터 만들어서 위쪽으로 조립해 나가는
'상향식(Bottom-up)' 접근 방식이 필요합니다. 각 노드는 불변하기 때문에,
부모 노드를 만들려면 자식 노드들이 이미 완전히 생성되어 팩토리 메서드의 인자로
전달될 수 있어야 합니다. 이는 자연스럽게 상향식 구성 방식으로 이어집니다.
앞서 예로 들었던 1 + 2라는 간단한 식을 API를 통해 직접 만들어 보겠습니다.

using System.Linq.Expressions;

// 1. 가장 아래쪽의 상수(Constant) 노드를 생성합니다.
// 상수 1을 나타내는 노드
var one = Expression.Constant(1, typeof(int));
// 상수 2를 나타내는 노드
var two = Expression.Constant(2, typeof(int));

// 2. 생성된 상수 노드들을 사용하여 덧셈(Add) 노드를 생성합니다.
// 'one'과 'two' 노드를 자식으로 갖는 덧셈 노드
var addition = Expression.Add(one, two);

// 3. 전체 식을 람다 식으로 감싸 최종 식 트리를 완성합니다.
// 매개변수가 없는 람다 식으로 최종 마무리
var lambda = Expression.Lambda(addition);

5)정리

우리는 이 글을 통해 C#의 식 트리가 단순한 코드가 아니라,
'코드를 데이터처럼 다루는' 강력한 기술임을 확인했습니다.

핵심 개념식 트리(Expression Tree)
본질코드를 트리 형태의 데이터 구조로 표현한 것
특징1. 한번 생성된 노드는 변경 불가 (Immutable)
2. 노드 단위로 코드 구조를 분석 및 변환
생성 방법1. 람다식을 컴파일러가 자동 생성
2. API를 사용하여 수동으로 직접 조립
주 사용처언어 간 번역: 동적 쿼리 생성 (LINQ to SQL)
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글