brunch

제23장: 인터프리터 패턴

언어를 이해하는 마법

by jeromeNa

국제회의에서 통역사가 하는 일을 생각해 보죠. 한국어로 말하는 발표자의 말을 듣고, 즉시 영어로 번역하여 청중에게 전달합니다. 하지만 단순히 단어만 바꾸는 것이 아닙니다. 문장 구조를 분석하고, 문맥을 파악하며, 문화적 뉘앙스까지 고려해서 의미를 전달해야 합니다.


"안녕하세요"라는 간단한 인사말도 상황에 따라 "Hello", "Good morning", "Nice to meet you" 등으로 다르게 번역됩니다. 통역사는 단어 사전을 뒤지는 것이 아니라, 언어의 규칙과 문법을 이해하고 있기 때문에 이런 유연한 번역이 가능합니다.


더 대단한 부분은 통역사가 새로운 표현을 만날 때의 반응입니다. 처음 듣는 은어나 전문용어라도, 문맥과 문법 구조를 바탕으로 의미를 추론하고 적절한 번역을 찾아냅니다. 이는 언어의 근본적인 구조와 규칙을 이해하고 있기 때문입니다.


프로그래밍적으로 본다면 언어의 문법을 객체로 표현하고, 그 규칙들을 조합하여 복잡한 표현을 해석할 수 있게 하는 것이 '인터프리터 패턴(Interpreter Pattern)'의 핵심 아이디어입니다.




1950년대 중반, 노암 촘스키(Noam Chomsky)는 형식 언어 이론을 제시했고, 1959년 존 백커스(John Backus)는 BNF(Backus-Naur Form) 문법 표기법을 도입했습니다.


1950년대 중반, IBM에서 FORTRAN 언어를 개발하던 백커스는 FORTRAN 개발 이후, ALGOL 언어의 문법 정의를 위해 BNF를 만들었습니다.


거의 동시에 MIT의 언어학자 촘스키는 언어의 구조를 수학적으로 분석하는 이론을 발표했습니다. 그는 모든 언어가 규칙의 조합으로 이루어져 있으며, 이 규칙들을 이용해 무한히 많은 문장을 생성하고 해석할 수 있다고 주장했습니다.


1960년대 후반, 컴파일러 이론이 발전하면서 "파싱(Parsing)"과 "해석(Interpretation)"이 분리되기 시작했습니다. 파싱은 문장을 구조적으로 분석하는 것이고, 해석은 그 구조를 실제 의미나 행동으로 변환하는 것입니다.. 이 분리는 언어 처리 시스템의 유연성을 크게 향상했습니다.


1970년대 초, 벨 연구소의 브라이언 커니핸(Brian Kernighan)과 데니스 리치(Dennis Ritchie)는 UNIX 시스템에서 정규표현식과 간단한 해석기를 도입하며 명령어 처리 방식의 모듈화를 실현했습니다.


1980년대 말, Ralph Johnson 등은 Smalltalk 환경에서 객체지향 설계 기법을 발전시켰고, 이는 1994년 GOF 디자인 패턴으로 정리됩니다. 특히 Ralph Johnson은 수학 표현식 계산기를 만들면서 각 연산자와 피연산자를 별도의 클래스로 표현하는 방법을 고안하죠. 이는 나중에 인터프리터 패턴의 핵심 구조가 되었습니다.


LISP는 프로그램과 데이터를 동일 구조로 처리하는 메타프로그래밍적 특징을 지녔으며, Interpreter 패턴과 유사한 동작 방식을 보였습니다.




수학 표현식을 해석하는 계산기를 예제로 보겠습니다.


// 표현식 인터페이스
interface Expression {
int interpret();
String toString();
}

// 숫자 표현식 (Terminal Expression)
class NumberExpression implements Expression {
private int number;

public NumberExpression(int number) {
this.number = number;
}

@Override
public int interpret() {
return number;
}

@Override
public String toString() {
return String.valueOf(number);
}
}

// 덧셈 표현식 (Non-terminal Expression)
class AddExpression implements Expression {
private Expression left;
private Expression right;

public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret() {
return left.interpret() + right.interpret();
}

@Override
public String toString() {
return "(" + left + " + " + right + ")";
}
}

// 뺄셈 표현식
class SubtractExpression implements Expression {
private Expression left;
private Expression right;

public SubtractExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret() {
return left.interpret() - right.interpret();
}

@Override
public String toString() {
return "(" + left + " - " + right + ")";
}
}

// 곱셈 표현식
class MultiplyExpression implements Expression {
private Expression left;
private Expression right;

public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret() {
return left.interpret() * right.interpret();
}

@Override
public String toString() {
return "(" + left + " * " + right + ")";
}
}

// 표현식 파서 (문자열을 Expression으로 변환)
class ExpressionParser {

public static Expression parse(String expression) {
// 간단한 파서 구현 (실제로는 더 복잡한 파싱 로직 필요)
return parseExpression(expression.replace(" ", ""));
}

private static Expression parseExpression(String expr) {
// 괄호 처리
if (expr.startsWith("(") && expr.endsWith(")")) {
expr = expr.substring(1, expr.length() - 1);
}

// 연산자 찾기 (우선순위 낮은 것부터)
for (int i = expr.length() - 1; i >= 0; i--) {
char c = expr.charAt(i);
if (c == '+' || c == '-') {
Expression left = parseExpression(expr.substring(0, i));
Expression right = parseExpression(expr.substring(i + 1));
return c == '+' ? new AddExpression(left, right) :
new SubtractExpression(left, right);
}
}

// 곱셈 처리
for (int i = expr.length() - 1; i >= 0; i--) {
char c = expr.charAt(i);
if (c == '*') {
Expression left = parseExpression(expr.substring(0, i));
Expression right = parseExpression(expr.substring(i + 1));
return new MultiplyExpression(left, right);
}
}

// 숫자 처리
return new NumberExpression(Integer.parseInt(expr));
}
}

// 수학 계산기 데모
public class MathCalculatorDemo {
public static void main(String[] args) {
System.out.println("� 수학 표현식 인터프리터 데모");
System.out.println("=".repeat(40));

// 다양한 수학 표현식 테스트
String[] expressions = {
"5",
"3+4",
"10-3",
"2*6",
"3+4*2",
"10-3+2",
"2*3+4*5"
};

for (String exprStr : expressions) {
try {
System.out.println("\n� 표현식: " + exprStr);

// 표현식 파싱
Expression expr = ExpressionParser.parse(exprStr);
System.out.println("� 파싱 결과: " + expr);

// 표현식 해석 (계산)
int result = expr.interpret();
System.out.println("✅ 계산 결과: " + result);

} catch (Exception e) {
System.out.println("❌ 오류: " + e.getMessage());
}
}

System.out.println("\n" + "=".repeat(40));
System.out.println("� 표현식 트리 구조 시각화");
System.out.println("=".repeat(40));

// 복잡한 표현식의 트리 구조 보여주기
Expression complexExpr = new AddExpression(
new MultiplyExpression(
new NumberExpression(3),
new NumberExpression(4)
),
new SubtractExpression(
new NumberExpression(10),
new NumberExpression(2)
)
);

System.out.println("\n� 표현식 트리: " + complexExpr);
System.out.println("� 단계별 계산:");
System.out.println(" 1. 3 * 4 = " +
new MultiplyExpression(new NumberExpression(3), new NumberExpression(4)).interpret());
System.out.println(" 2. 10 - 2 = " +
new SubtractExpression(new NumberExpression(10), new NumberExpression(2)).interpret());
System.out.println(" 3. 12 + 8 = " + complexExpr.interpret());

System.out.println("\n✅ 인터프리터 패턴 데모 완료!");
}
}


위 코드에서 주목해야 할 부분은 수학 표현식의 문법 규칙이 그대로 클래스 구조로 표현된다는 점입니다.


각 수학 연산자(+, -, *)가 별도의 클래스로 표현됩니다. 이는 BNF 문법의 각 규칙이 클래스로 변환된 것입니다. 새로운 연산자(/, ^, % 등)를 추가하려면 새로운 클래스만 만들면 됩니다.


`AddExpression`은 두 개의 `Expression`을 받습니다. 이 Expression들은 숫자일 수도 있고, 다른 연산 표현식일 수도 있습니다. 이런 재귀적 구조로 인해 복잡한 중첩 표현식도 자연스럽게 처리됩니다.


"3+4*2"라는 표현식은 먼저 문법 트리로 파싱 되고, 그다음 `interpret()` 메서드를 통해 실제 계산이 수행됩니다. 파싱과 해석이 분리되어 있어 각각을 독립적으로 수정할 수 있습니다.


새로운 연산자나 함수를 추가하기 위해서는 새로운 Expression 클래스만 만들면 됩니다. 기존 코드는 전혀 수정하지 않아도 됩니다.




30년 전 Gang of Four가 제시한 인터프리터 패턴은 오늘날 SQL 처리기, 설정 파일 파서, 도메인 특화 언어(DSL)의 핵심 구조가 되었습니다. 이는 단순히 기술적 우수성 때문만이 아닙니다. 이 패턴이 담고 있는 철학 - 규칙의 이해와 조합, 단계적 해석, 구조화된 소통 - 이 인간의 근본적인 소통 방식을 반영하기 때문입니다.


복잡한 상황을 단순한 규칙들로 분해하고, 그 규칙들을 조합하여 해결책을 찾아내는 능력을 기르고, 다른 사람들과 소통할 때도 명확하고 구조화된 언어를 사용하여 의미를 정확히 전달해야 합니다.


통역사가 언어의 규칙을 이해하여 문화와 문화 사이의 다리 역할을 하듯이, 진정한 소통은 단어를 바꾸는 것이 아니라, 규칙을 이해하고 의미를 전달하는 것입니다.




keyword