S E P H ' S
[행동 패턴] 인터프리터(Interpreter) 패턴 본문
인터프리터(Interpreter) 패턴
인터프리터 패턴이란?
프로그램을 작성할 때 프로그램의 모든 행위를 정의하지 못하는 경우도 있다. 예를 들어서 브라우저를 만들때, 사이트 디자이너가 웹 페이지를 어떻게 행동하게 하고 싶어할지에 대해서 모두 예측하는 것은 불가능하다. 그럴때 JavaScript와 같은 인터프리터 언어를 통해서 브라우저에 브라우저 프로그래머가 구현하지 않은 행위를 추가할 수 있도록 한다.
인터프리터 패턴은 이러한 용도에 사용되는 인터프리터를 작성할 수 있도록 해준다. 먼저 언어의 문법을 기술하는 규칙들에 대한 형식 문법을 정의한다. 그리고 각 규칙들을 클래스를 통해 구현한다. 이 클래스들은 Context 객체를 공유하며, Context 객체를 통해 입력을 받고 변수 값을 저장하는 등의 작업을 한다.
언제 사용할까?
- 객체지향 컴파일러 구현에 널리 사용된다.
- 컴포지트 패턴이 사용되는 곳에 인터프리터 패턴을 사용할 수 있다. 하지만 컴포지트 패턴으로 정의한 클래스들이 하나의 언어 구조를 정의할 때만 인터프리터 패턴이라고 할 수 있다.
- 시스템 설정을 런타임에 변경하기 위해 사용되는 경우도 있다.
- 언어가 주어지면 해당 표현을 사용하여 언어로 문장을 해석하는 인터프리터를 사용하여 문법 표현을 정의한다.
구조
- 해석기 정보(Context) : 모든 Expression에서 사용하는 공통 정보가 담겨있다.
- 추상 구문 트리 인터페이스 (AbstractExpression) : 우리가 표현하는 문법을 나타낸다. Context가 들어있다.
- 종료 기호(Terminal Expression) : 종료되는 Expression
- 비종료 기호(Non-Terminal Expression) : 다른 Expression들을 재귀적으로 참조하고 있는 Expression
- 문장을 나타내는 추상 구문 트리(Client)
장단점
장점
- 각 문법 규칙을 클래스로 표현하기 때문에 언어를 쉽게 구현할 수 있다.
- 문법이 클래스에 의해 표현되기 때문에 언어를 쉽게 변경하거나 확장할 수 있다.
- 클래스 구조에 메소드만 추가하면 프로그램을 해석하는 기본 기능 외에 보기 쉽게 출력하는 기능이나 더 나은 프로그램 확인 기능 같은 새로운 기능을 추가할 수 있다.
단점
- 문법 규칙의 개수가 많아지면 복잡해진다.
- 다양한 문법이 생성될때 성능 저하가 발생한다.
예시
후위연산을 하는 코드를 통해 인터프리터 패턴을 알아보도록 하겠다. (후위 연산 : "123+-" -> "1 - (2 + 3)")
public class PostfixNotation {
private final String expression;
public PostfixNotation(String expression) { this.expression = expression; }
public static void main(String[] args) {
PostfixNotation postfixNotation = new PostfixNotation("123+-");
postfixNotation.calculate();
}
private void calculate() {
Stack<Integer> numbers = new Stack<>();
for (char c: this.expression.toCharArray()) {
switch (c) {
case '+':
numbers.push(numbers.pop() + numbers.pop());
break;
case '-':
int right = numbers.pop();
int left = numbers.pop();
numbers.push(left - right);
break;
default:
numbers.push(Integer.parseInt(c + ""));
}
}
System.out.println(numbers.pop());
}
}
"123+-"와 같은 연산, 즉 "xyz+-"를 문법으로 정의하여 재사용할 것이라고 가정해보자.
x, y, z는 그대로 자신의 값들을 반환한다. 자신의 값들을 반환하면서 다음 단계는 없는 TerminalExpression이다.
반면 연산자인 '+', '-'는 다른 두개의 Expression을 interpret(숫자면 숫자, 문자면 문자로 치환한다고 생각하면 된다.)한 다음 그 결과를 연산하는 Non-Terminal Expression이다.
이 예시를 사용해서 문자열 후위 연산을 하도록 인터프리터 패턴을 구현해보도록 할 것이다.
먼저 PostFixExpression 인터페이스를 만든다.
public interface PostFixExpression {
int interpret(Map<Character, Integer> context);
}
모든 Expression들은 위의 인터페이스를 구현하면 된다.
먼저 값을 그대로 반환하는 Terminal Expression인 VariableExpression을 만든다.
public class VariableExpression implements PostFixExpression {
private Character character;
public VariableExpression(Character character) {
this.character = character;
}
@Override
public int interpret(Map<Character, Integer> context) {
return context.get(this.character); // 생성자 인자로 받은 character 의 키를 가져온다.
}
}
다음으로 Non-Terminal Experssion인 Plus, Minus Expression을 만든다.
public class PlusExpression implements PostFixExpression{
private PostFixExpression left;
private PostFixExpression right;
public PlusExpression(PostFixExpression left, PlusExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Map<Character, Integer> context) {
return left.interpret(context) + right.interpret(context);
}
}
public 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);
}
}
이제 Expression들을 사용하여 해석 작업을 하는 Parser 클래스를 만들 것이다. 예시에 나왔던 것처럼 스택을 사용하여 구현한다.
public class PostFixParser {
public static PostFixExpression parse(String expression) {
Stack<PostFixExpression> stack = new Stack<>();
for (char c: expression.toCharArray()) {
stack.push(getExpression(c, stack));
}
return stack.pop();
}
private static PostFixExpression getExpression(char c, Stack<PostFixExpression> stack) {
switch (c) {
case '+' :
return new PlusExpression(stack.pop(), stack.pop());
case '-' :
// 스택에서 먼저 빠져 나오는 값부터 계산해야 후위 연산의 순서가 맞게 된다.
PostFixExpression right = stack.pop();
PostFixExpression left = stack.pop();
return new MinusExpression(left, right);
default:
return new VariableExpression(c);
}
}
}
테스트
사용자의 입력값에 따라 후위연산으로 계산해주도록 App 클래스를 구현했다. 문자(a ~ z)까지 1~26의 숫자로 해석하여 계산해주도록 구현했다.
public class App {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Map<Character, Integer> map = new HashMap<>();
System.out.println("알파벳 소문자와 '+', '-' 만 입력해주세요.");
String input = br.readLine();
for (char c: input.toCharArray()) {
if (c != '+' && c != '-') {
map.put(c, c - 96);
}
}
for (Map.Entry<Character, Integer> entrySet: map.entrySet()) {
System.out.println(entrySet.getKey() + " : " + entrySet.getValue());
}
// String 입력값을 넣는다.
PostFixExpression expression = PostFixParser.parse(input);
int res = expression.interpret(map);
System.out.println(res);
}
}
정리
- 자주 사용되는 패턴은 아니나 이진수, 십진수 등 해석이 빈번하게 필요한 프로그램 같은 경우 유용하게 사용할 수 있다.
- 가장 좋은 예시는 JVM이 자바 소스코드를 이해하도록 바이트 코드로 변환해주는 자바 컴파일러이다.
- 또한 자바의 정규표현식이나 스프링의 Expression Language 등이 있다.
'Programing & Coding > Design Pattern' 카테고리의 다른 글
[행동 패턴] 커맨드(Command) 패턴 (0) | 2023.03.26 |
---|---|
[구조 패턴] 데코레이터(Decorator) 패턴 (0) | 2023.03.26 |
[행동 패턴] 방문자(Visitor) 패턴 (0) | 2023.03.19 |
[행동 패턴] 템플릿 메소드(Template Method) 패턴 (0) | 2023.03.19 |
[행동 패턴] 상태(State) 패턴 (0) | 2023.03.19 |