프로그래밍/디자인패턴

인터프리터 패턴(Interpreter Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 19:22

"우리 회사만의 업무 규칙(DSL)을 만들고 싶어요."

"사용자가 입력한 수식(10 + 20 - 5)을 계산하는 계산기를 만들어야 해요."

단순한 문자열을 컴퓨터가 이해할 수 있는 동작으로 변환하려면 **문법(Grammar)**을 정의하고 이를 **해석(Interpret)**해야 합니다. 이때 문법 규칙 하나하나를 객체로 만들어 조립하는 것이 인터프리터 패턴입니다.

하지만 규칙이 많아질수록 클래스가 폭발적으로 늘어나는 문제가 있습니다. 이를 해결하는 두 가지 방법을 비교해 봅니다.


1. 방식 1: 클래스 기반 AST (GoF 정석) - "구조적이지만 무거움"

가장 교과서적인 방식입니다. 문법의 각 요소(숫자, 더하기, 빼기 등)를 클래스로 정의하고, 이를 트리 구조(Abstract Syntax Tree, AST)로 엮어서 실행합니다.

구조

  • Expression: 모든 구문이 구현해야 할 공통 인터페이스 (interpret).
  • Terminal Expression: 더 이상 쪼개지지 않는 요소 (예: 숫자).
  • Non-Terminal Expression: 다른 표현식을 포함하는 요소 (예: 덧셈, 뺄셈).

코드 예시 (후위 표기법 계산기: 1 2 + -> 3)

Java
 
// 1. 표현식 인터페이스
interface Expression {
    int interpret();
}

// 2. 종단 표현식 (숫자)
class NumberExpression implements Expression {
    private int number;
    public NumberExpression(int number) { this.number = number; }
    
    @Override
    public int interpret() { return number; }
}

// 3. 비종단 표현식 (덧셈)
class PlusExpression implements Expression {
    private Expression left, right;
    
    public PlusExpression(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }
    
    @Override
    public int interpret() {
        return left.interpret() + right.interpret();
    }
}

// 4. 사용 (Client)
// "1 + 2"를 파싱하여 트리로 구성했다고 가정
Expression two = new NumberExpression(2);
Expression one = new NumberExpression(1);
Expression plus = new PlusExpression(one, two);

System.out.println(plus.interpret()); // 3

장단점

  • 장점: 문법 구조가 명확하게(트리 형태) 보입니다. 새로운 규칙(예: 곱셈)을 추가하려면 클래스 하나만 더 만들면 됩니다.
  • 단점: 클래스 폭발(Class Explosion). 문법 규칙이 100개면 클래스도 100개가 필요합니다. 코드가 매우 방대해집니다.

2. 방식 2: 함수형 인터프리터 (Lambda) - "가볍고 실용적"

자바 8 이후에는 굳이 PlusExpression 같은 작은 클래스를 일일이 만들지 않습니다. **람다(Lambda)와 맵(Map)**을 이용해 규칙을 함수로 정의하여 처리합니다.

특징

  • 별도의 클래스 파일 없이, 연산 규칙을 Map<String, BiFunction> 등에 담아 관리합니다.

코드 예시

Java
 
public class FunctionalCalculator {
    // 연산 규칙을 람다로 정의 (확장이 매우 쉬움)
    private static final Map<String, BiFunction<Integer, Integer, Integer>> operators = new HashMap<>();
    
    static {
        operators.put("+", (a, b) -> a + b);
        operators.put("-", (a, b) -> a - b);
        operators.put("*", (a, b) -> a * b); // 클래스 생성 없이 한 줄로 추가 끝!
    }

    public static int calculate(String operator, int a, int b) {
        return operators.get(operator).apply(a, b);
    }
}

// 사용
int result = FunctionalCalculator.calculate("+", 1, 2);
System.out.println(result); // 3

장단점

  • 장점: 클래스 파일이 사라지고 코드가 획기적으로 줄어듭니다. 로직이 한눈에 들어옵니다.
  • 단점: AST(트리) 구조를 명시적으로 보여주기보다는, 즉시 계산하는 로직에 가깝습니다. 복잡한 문법 구조(중첩된 괄호 등)를 처리하려면 결국 스택이나 재귀 로직이 추가로 필요합니다.

3. 실무 예시: 언제 쓰이나요?

1) SQL 파싱 (Hibernate/JPA)

우리가 작성한 JPQL(SELECT m FROM Member m)은 하이버네이트 내부의 인터프리터가 해석하여 데이터베이스별 SQL(SELECT * FROM users)로 변환합니다. 이때 거대한 AST가 생성됩니다.

2) Spring Expression Language (SpEL)

@Value("#{systemProperties['user.timezone']}") 처럼 스프링 설정에서 쓰는 표현식도 내부적으로 인터프리터 패턴을 사용해 파싱 되고 실행됩니다.

3) 정규표현식 (Pattern/Matcher)

Pattern.compile("a*b")를 호출하면 내부적으로 이 문자열 규칙을 해석하는 인터프리터 엔진 객체(상태 머신)가 생성됩니다.


요약: 직접 만들까, 람다를 쓸까, 라이브러리를 쓸까?

구분 Class-based AST (GoF) Functional (Lambda) Expression Engine (SpEL)
구조 인터페이스 + 구현 클래스 다수 Map + 람다식 외부 라이브러리 활용
확장성 높음 (새 클래스 추가) 높음 (Map에 put) 낮음 (제공된 문법만 사용)
복잡도 매우 높음 (클래스 많음) 낮음 (코드 간결) 최하 (구현 필요 없음)
추천 상황 컴파일러, 복잡한 언어 파서 제작 간단한 사칙연산, 규칙 엔진 복잡한 수식이나 로직을 문자열로 처리할 때

결론

"간단한 사칙연산기나 규칙 엔진을 만들고 싶다"면 **함수형 방식(Lambda)**이 정답입니다. 클래스 수십 개를 만드는 건 과도한 엔지니어링(Over-engineering)입니다.

하지만 "진짜 프로그래밍 언어나 복잡한 쿼리 파서를 만들어야 한다"면 클래스 기반 AST 방식을 써야 합니다.

(물론, 실무에서는 웬만하면 SpEL이나 JEXL 같은 검증된 표현식 라이브러리를 가져다 쓰는 것이 정신건강에 가장 이롭습니다.)