안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 행동 패턴(Behavioral Patterns)의 한 종류로, 간단한 언어의 문법을 정의하고 그 문장을 해석하는 방법을 제공하는 인터프리터(Interpreter) 패턴에 대해 알아보겠습니다.
정의
인터프리터(Interpreter) 패턴은 특정 언어에 대해, 그 언어의 문법을 위한 표현(Representation)을 정의하고, 이 표현을 사용하여 문장을 해석하는 해석기(Interpreter)를 만드는 패턴입니다. 즉, 해결하려는 특정 문제에 대해 간단한 '미니 언어'를 만들고, 그 언어의 규칙에 따라 동작하는 프로그램을 만드는 방식입니다.
예를 들어, "A 그리고 B" 또는 "C 또는 (D 그리고 E)"와 같은 논리식을 계산해야 한다고 생각해봅시다. 여기서 '그리고', '또는'은 연산자이고, A, B, C 등은 피연산자입니다. 인터프리터 패턴은 이러한 문법 구조(논리식)를 클래스 구조로 표현합니다. '그리고' 연산을 위한 클래스, '또는' 연산을 위한 클래스, 피연산자를 위한 클래스 등을 만들어, 이 클래스들을 조합(트리 구조)하여 전체 논리식을 해석하고 결과를 도출합니다. 컴파일러나 SQL 파서처럼 복잡한 언어를 해석하는 데에도 이 패턴의 원리가 적용되지만, 주로 해결해야 할 문제가 특정 패턴의 언어로 표현될 수 있을 때 유용하게 사용됩니다.
인터프리터 패턴은 보통 컴포지트 패턴과 유사한 트리 구조를 가집니다.
- AbstractExpression (추상 표현식): 언어의 모든 문법 요소(터미널, 논터미널)가 구현해야 할 공통 인터페이스입니다. 보통 interpret()라는 해석 메서드를 정의합니다.
- TerminalExpression (종단 표현식): 언어의 가장 기본적인 문법 요소(종단 기호)를 나타냅니다. 트리 구조의 잎(Leaf) 노드에 해당합니다. (예: 변수, 상수)
- NonterminalExpression (비종단 표현식): 다른 표현식들을 자식으로 가지는 문법 규칙을 나타냅니다. 트리 구조에서 가지(Branch) 노드에 해당하며, 자식 표현식들의 결과를 조합하여 자신의 결과를 만듭니다. (예: 덧셈, 뺄셈 연산자)
- Context (문맥): 인터프리터가 문장을 해석하는 데 필요한 전역 정보(예: 변수의 값)를 담고 있습니다.
- Client (클라이언트): 해석할 문장을 받아 파싱하여 추상 구문 트리(AST)를 구성하고, interpret() 메서드를 호출하여 해석을 시작합니다.
사용예시
후위 표기법(Postfix Notation)으로 작성된 간단한 수식을 계산하는 예제를 통해 인터프리터 패턴을 구현해 보겠습니다. (예: "4 3 2 - 1 + *")
1. 추상 표현식 (AbstractExpression) 인터페이스 정의
모든 표현식(숫자, 연산자)이 공통으로 가질 인터페이스를 정의합니다.
import java.util.Map;
// AbstractExpression 인터페이스
public interface PostfixExpression {
int interpret(Map<Character, Integer> context);
}
2. 터미널 및 비종단 표현식 (Terminal & Nonterminal) 클래스 구현
숫자를 나타내는 NumberExpression과 연산자를 나타내는 PlusExpression, MinusExpression 등을 구현합니다.
// TerminalExpression: 숫자를 나타내는 표현식
class NumberExpression implements PostfixExpression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Map<Character, Integer> context) {
return this.number;
}
}
// NonterminalExpression: 덧셈 연산
class PlusExpression implements PostfixExpression {
private PostfixExpression left;
private PostfixExpression right;
public PlusExpression(PostfixExpression left, PostfixExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Map<Character, Integer> context) {
return left.interpret(context) + right.interpret(context);
}
}
// NonterminalExpression: 뺄셈 연산
class MinusExpression implements PostfixExpression {
private PostfixExpression left;
private PostfixExpression right;
public MinusExpression(PostfixExpression left, PostfixExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Map<Character, Integer> context) {
return left.interpret(context) - right.interpret(context);
}
}
3. 클라이언트 (Client) 코드
클라이언트는 후위 표기법 문자열을 파싱하여 표현식 트리를 만들고, 최종 결과를 계산합니다.
import java.util.Stack;
public class PostfixParser {
public static PostfixExpression parse(String expression) {
Stack<PostfixExpression> stack = new Stack<>();
for (char c : expression.toCharArray()) {
// 연산자를 만나면 스택에서 두 개의 표현식을 꺼내 새로운 연산 표현식을 만듦
if (isOperator(c)) {
PostfixExpression right = stack.pop();
PostfixExpression left = stack.pop();
stack.push(getOperatorInstance(c, left, right));
}
// 숫자를 만나면 숫자 표현식을 만들어 스택에 넣음
else if (Character.isDigit(c)) {
stack.push(new NumberExpression(Character.getNumericValue(c)));
}
}
return stack.pop();
}
private static boolean isOperator(char c) {
return c == '+' || c == '-';
}
private static PostfixExpression getOperatorInstance(char c, PostfixExpression left, PostfixExpression right) {
switch (c) {
case '+':
return new PlusExpression(left, right);
case '-':
return new MinusExpression(left, right);
}
return null;
}
public static void main(String[] args) {
String expression = "43-2+"; // (4-3)+2
PostfixExpression parsedExpression = parse(expression);
// Context는 이 예제에서 사용되지 않음
int result = parsedExpression.interpret(null);
System.out.println("결과: " + result); // 결과: 3
}
}
결론
인터프리터 패턴은 다음의 경우에 사용됩니다
- 해결하려는 문제를 간단한 언어로 표현할 수 있을 때: SQL, 정규 표현식처럼 특정 도메인에 특화된 언어를 처리해야 할 때 유용합니다.
- 언어의 문법이 비교적 단순할 때: 문법이 복잡해지면 클래스 계층이 너무 깊고 복잡해져 관리하기 어렵습니다.
- 문법을 변경하거나 새로운 표현식을 추가하는 것이 쉬워야 할 때: 새로운 AbstractExpression의 서브클래스를 만드는 것으로 쉽게 문법을 확장할 수 있습니다.
그러나 다음의 사항을 고려해야합니다.
- 복잡한 문법 처리의 어려움: 문법 규칙이 많아지면 클래스의 수가 급격히 늘어나고 시스템이 매우 복잡해집니다. 이런 경우에는 파서 생성기(Parser Generator) 같은 전문적인 도구를 사용하는 것이 더 효율적입니다.
- 성능 문제: 재귀적인 호출 구조로 인해 성능이 중요한 시스템에는 적합하지 않을 수 있습니다.
인터프리터 패턴은 특정 도메인의 문제를 언어적인 관점에서 해결하는 독특한 방법을 제공합니다. 비록 모든 상황에 적합한 만능 해결책은 아니지만, 적절한 문제에 적용했을 때 확장성 있는 설계를 가능하게 합니다. 다음 시간에는 객체상태기반의 패턴, State 패턴을 알아보겠습니다.
'정보보안-이론 > XX에 대하여' 카테고리의 다른 글
[디자인 패턴] (GoF 행위패턴) Strategy Pattern에 대하여 (0) | 2025.07.13 |
---|---|
[디자인 패턴] (GoF 행위패턴) State Pattern에 대하여 (0) | 2025.07.12 |
[디자인 패턴] (GoF 행위패턴) Iterator Pattern에 대하여 (0) | 2025.07.11 |
[디자인 패턴] (GoF 행위패턴) Memento Pattern에 대하여 (0) | 2025.07.11 |
[디자인 패턴] (GoF 행위패턴) Mediator Pattern에 대하여 (0) | 2025.07.10 |