인터프리터 패턴 (Interpreter Pattern) 이란?
- 인터프리터의 의미 자체는 보통 해석해주거나 통역해주는 역할을 가진 사람 혹은 물건이다.
- 악보를 음악으로 변환하는 것도 이러한 역할의 하나이기 때문에 연주자라는 의미도 있다.
- 가장 쉽게 볼 수 있는 예로는 정규표현식이 있다.
- 자주 등장하는 문제를 별개의 언어로 정의하고 재사용하는 패턴이다.
- 문법에 등장하는 규칙을 클래스로 표현하고 언어에서의 표현식을 해석하고 평가한다.
Expression
이라는 추상 클래스를 만드는 경우가 많다.Expression
클래스에서 파생된 구체적인 클래스는 언어의 다양한 규칙 또는 요소를 나타낸다.
- 반복되는 문제 패턴을 언어 또는 문법으로 정의하고 확장할 수 있다.
다이어그램으로 살펴보기
- 컴포짓 패턴과 굉장히 유사하게 생겼고, 컴포짓 패턴이 그리는 트리인 AST 가 나오게 된다.
interpret(Context)
에서 항상Context
가 있음이 중요하다.TerminalExpression
은 종료가 가능한Expression
이다.- 숫자 연산을 할 때 숫자 그 자체를 들 수 있다.
- AST 에서 트리의 leaf node 로 표현될 수 있는 것이다.
NonTerminalExpression
은 다른Expression
을 참조하는Expression
이다.- 숫자 연산을 할 때 숫자를 연산하는 기호를 들 수 있다.
- 그 자체로 끝날 수 없다.
예제: Postfix 표기법 (Postfix Notation)
- 우리가 평소에 수학 문제를 풀 때는
10
-
5
와 같이 부호를 가운데에 두고 양 옆에 숫자를 둔다.- 이를 Infix 표기법 (Infix Notation) 이라고 한다.
- Prefix 표기법에서는
10
5
-
처럼 부호가 뒤에 온다. - 얼핏보면 아무런 의미 없어보이지만 자신만의 문법을 만든 것이다.
- 이렇게 도메인에 특화된 언어를 DSL (도메인 특화 언어) 이라고 한다.
Before: 스택을 이용한 알고리즘으로 구현
- 스택을 이용해 구현했고 이 방식도 잘 작동한다.
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());
}
}
After: 문법을 만들어 구현
- Before 와 정확히 같은 역할을 하는 코드이다.
- 다만, 개개의 케이스에 어떠한 타입으로 식별될 것인지 자바 코드로 정의했다.
VariableExpression
은 이전에 봤던TerminalExpression
이다.PlusExpression
,MinusExpression
은NonTerminalExpression
이다.
- 특정 도메인에 특화된 언어처럼 클래스를 설계했다.
public class App {
public static void main(String[] args) {
PostfixExpression expression = PostfixParser.parse("xyz+-a+");
int result = expression.interpret(Map.of('x', 1, 'y', 2, 'z', 3, 'a', 4));
System.out.println(result);
}
}
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);
}
}
}
public interface PostfixExpression {
int interpret(Map<Character, Integer> context);
}
public 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);
}
}
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);
}
}
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);
}
}
장점
명확한 관심사 분리
- 문법과 해석을 기본 로직에서 분리하여 별도의 클래스로 캡슐화 되므로 모듈화되어 유지보수하기 쉬워진다.
쉬운 확장
Expression
클래스에서 파생된 새로운 구현 클래스만 추가하면 DSL 을 쉽게 확장할 수 있다.
가독성
- 문법과 규칙을 계층구조로 명시적으로 모델링하기 때문에 코드의 가독성이 높아진다.
- 문법과 요소의 관계를 이해하는데 도움이 된다.
재사용 가능성
- DSL 의 여러 인스턴스에 대해 동일한 표현식 클래스와 인터프리터 로직을 사용할 수 있으므로 재사용성을 촉진한다.
단점
복잡성
- DSL 이 크고 복잡한 경우 많은 별도의 클래스가 생기므로 관리 및 이해가 어려워질 수도 있다.
성능
- 종종 재귀와 객체 생성을 포함하므로 규모가 크거나 복잡한 표현식의 경우 오버헤드가 발생한다.
- 몇몇 사례에서는 인터프리터 패턴이 효율적인 해답이 아닐 수 있다.
제한된 적용 가능성
- 문법이 너무 자주 진화하는 경우엔 유지 관리하는데에 비용이 더 들 수도 있다.
- 주로 간단하고 잘 정의된 문법에 효과적이다.
불완전한 솔루션
- 인터프리터 패턴만으로는 렉싱, 에러 핸들링, 최적화와 같은 케이스의 측면을 처리하기 어려울 수 있다.
- 경우에 따라 완벽한 솔루션을 얻기 위해 다른 기술이나 도구와 결합해야 할 수도 있다.
자바와 스프링에서 찾아보는 패턴
자바
- 자바의 컴파일러
- 정규표현식
public static void main(String[] args) {
System.out.println(Pattern.matches(".pr...", "spring"));
System.out.println(Pattern.matches("[a-z]{6}", "spring"));
System.out.println(Pattern.matches("seo", "jakeseo"));
System.out.println(Pattern.matches("\\d", "1")); // one digit
System.out.println(Pattern.matches("\\D", "a")); // one non-digit
}
스프링
- 스프링의 SpEL
- 값을 설정하거나 메서드를 실행하거나 여러가지 역할을 할 수 있다.
public static void main(String[] args) {
Book book = new Book("spring");
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("title");
System.out.println(expression.getValue(book));
}
@Service
public class MyService implements ApplicationRunner {
@Value("#{2 + 5}") // SpEL
private String value;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println(value);
}
}
레퍼런스
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
중재자 패턴 (Mediator Pattern) 이란? (0) | 2023.05.09 |
---|---|
이터레이터 패턴 (Iterator Pattern) 이란? (0) | 2023.04.30 |
커맨드 패턴 (Command Pattern) 이란? (0) | 2023.04.28 |
책임 연쇄 패턴 (Chain Of Responsibility) 이란? (2) | 2023.04.26 |
프록시 패턴 (Proxy Pattern) 이란? (0) | 2023.04.25 |