목차
작은 언어를 반복 해석해야 한다
권한 규칙을 문자열로 다룬다고 하자. "admin AND active", "(vip OR staff) AND active" 같은 불리언 식이 계속 들어오고, 그때그때 참/거짓을 판정해야 한다.
if (parse("admin AND active", context)) { ... }이런 작은 식이 반복해서 등장한다. 문자열을 매번 if로 갈라 해석하는 코드를 손으로 짜면, 식이 조금만 복잡해져도(괄호, 우선순위, 중첩) 금세 엉킨다. 필요한 건 이 작은 언어의 문법을 코드로 표현하고, 식을 그 문법대로 평가하는 방법이다.
인터프리터 = 문법을 객체 트리로
인터프리터 패턴이 이걸 한다.
작은 언어의 문법 규칙 하나하나를 클래스로 만들고, 식을 그 클래스들의 객체 트리로 조립한 뒤, 트리를 따라 내려가며 해석(평가)한다.
admin AND active는 “AND 노드 아래 변수 둘”이라는 트리가 된다. 문법의 규칙이 곧 클래스이고, 하나의 식은 그 클래스들로 짜인 나무 한 그루다. 그 나무를 평가하면 결과가 나온다.
Expression 인터페이스
모든 문법 규칙이 공유하는 것은 “해석된다”는 것 하나다. 그래서 공통 인터페이스는 interpret 하나만 둔다.
public interface Expression {
boolean interpret(Map<String, Boolean> context);
}context는 변수의 실제 값이다. {admin=true, active=false} 같은 것. 어떤 규칙이든 이 컨텍스트를 받아 자기 자리에서 참/거짓을 낸다.
터미널과 논터미널
문법의 규칙은 두 갈래다.
- 터미널 표현식(terminal) - 더 쪼갤 수 없는 잎. 값이나 변수다. 여기선
Variable. - 논터미널 표현식(non-terminal) - 하위 표현식을 품어 조합하는 가지. 여기선
And,Or.
터미널은 컨텍스트에서 자기 값을 꺼낼 뿐이다.
public class Variable implements Expression {
private final String name;
public Variable(String name) { this.name = name; }
@Override
public boolean interpret(Map<String, Boolean> context) {
return context.getOrDefault(name, false);
}
}논터미널은 하위 표현식 둘을 품고, 그 둘의 해석 결과를 조합한다.
public class And implements Expression {
private final Expression left, right;
public And(Expression left, Expression right) {
this.left = left; this.right = right;
}
@Override
public boolean interpret(Map<String, Boolean> context) {
return left.interpret(context) && right.interpret(context);
}
}
public class Or implements Expression {
private final Expression left, right;
public Or(Expression left, Expression right) {
this.left = left; this.right = right;
}
@Override
public boolean interpret(Map<String, Boolean> context) {
return left.interpret(context) || right.interpret(context);
}
}And와 Or는 좌우가 무엇인지 모른다. 그저 interpret를 부를 뿐이다. 그래서 좌우에 또 다른 And나 Or가 와도 똑같이 돌아간다.
트리를 조립해 평가
(admin AND active) OR staff를 트리로 짜맞춘다.
Expression expr = new Or(
new And(new Variable("admin"), new Variable("active")),
new Variable("staff")
);
Map<String, Boolean> ctx = Map.of(
"admin", true, "active", false, "staff", true
);
boolean result = expr.interpret(ctx); // true식의 구조가 그대로 객체 트리의 모양이 된다. 괄호와 우선순위는 트리의 계층으로 표현된다.
재귀 해석
루트에서 interpret를 한 번 부르면, 평가가 트리를 타고 재귀로 내려간다.
Or.interpret가 왼쪽 And.interpret를 부르고, 그게 다시 두 Variable.interpret를 부른다. 잎에서 컨텍스트 값이 올라오고, 가지에서 &&·||로 합쳐지며, 루트까지 도로 접혀 올라와 최종 결과가 된다. 각 노드는 자기 몫만 안다. “왼쪽과 오른쪽을 해석해 AND로 묶는다”만 알 뿐, 전체 식이 얼마나 큰지는 모른다. 그 지역적인 규칙들이 트리로 쌓여 전체 해석이 완성된다.
한계: 문법이 커지면
솔직히 말하면 인터프리터는 실무에서 드물게 쓰이는 패턴이다. 문법이 조금만 커져도 규칙마다 클래스가 하나씩 늘어 클래스 폭발이 온다. 사칙연산에 함수 호출, 우선순위, 변수 대입까지 얹으면 클래스 수십 개가 필요하고, 그걸 손으로 유지하는 건 고통이다. 게다가 이 패턴은 문법을 트리로 표현할 뿐, 문자열을 트리로 바꾸는 파싱은 다루지 않는다. 파싱까지 손으로 짜면 부담은 더 커진다.
그래서 문법이 조금이라도 진지해지면 실무에선 파서 생성기나 전용 라이브러리를 쓴다(ANTLR 등). 인터프리터 패턴은 문법이 아주 작고 안정적일 때, 외부 도구를 끌어오기 아까울 때만 제 값을 한다. 이 패턴을 배우는 값어치는 실전에 자주 쓰기 위해서라기보다, “문법을 객체 트리로 보고 재귀로 평가한다”는 발상 자체에 있다.
실무: 규칙 엔진과 표현식 평가
발상이 드러나는 곳은 있다. 규칙 엔진은 사용자가 정의한 조건식을 트리로 만들어 평가하고, 간단한 DSL이나 필터 문법도 이 구조로 돈다. Spring의 SpEL(Spring Expression Language) 처럼 #user.age > 18 AND #user.active 같은 식을 파싱해 평가하는 것도, 안쪽을 들여다보면 식을 노드 트리로 만들어 재귀로 평가하는 이 발상이다. 직접 인터프리터 패턴으로 짜기보다, 이런 라이브러리 뒤에 이 구조가 있음을 알아보는 쪽에 쓸모가 있다.
정리
| 인터프리터란 | 문법 규칙을 클래스로 만들어 식을 객체 트리로 평가한다 |
| 문제 | 작은 언어의 식을 반복해서 해석해야 한다 |
| 구성 | 터미널(값·변수)과 논터미널(And·Or 등)이 Expression을 구현 |
| 평가 | 식을 트리로 조립하고 루트에서 interpret를 재귀로 내려보낸다 |
| 한계 | 문법이 커지면 클래스 폭발 - 실무는 파서 생성기를 쓴다 |
| 쓸모 | 규칙 엔진·간단한 DSL·SpEL 뒤에 있는 발상 |
작은 언어의 문법을 객체 트리로 세우고 그 트리를 재귀로 평가하는 것 - 드물게 쓰이지만 발상은 또렷한 게 인터프리터다.