인터프리터 (Interpreter) 패턴

weekbelt·2022년 12월 17일
0

1. 패턴 소개

인터프리터라는 용어는 ComputerScience에서 주로 사람이 작성한코드를 기계가 이해하기 쉬운형태로 변환해주는 장치를 뜻하는데 ComputerScience외적으로는 통역하는 사람들, 연주하는 사람들을 뜻합니다. 원본이 되는 무언가를 다른형태로 바꿔주는데 가령 연주자 같은 경우는 악보를 사람들이 들을 수 있는 음악으로 변환해주는 역할을 합니다.
인터프리터 패턴은 우리가 자주사용하는 어떤 패턴이이나 자주 해결해야하는 문제들을 일종의 패턴 혹은 언어, 문법을 정의합니다. 그리고 그 문법에 맞춰서 어떤표현식 이나 문자열을 작성하면 그것을 대입해서 문제를 해결하는 패턴입니다. 가령 정규표현식을 인터프리터라고 할 수 있습니다. 우리가 어떤 문서에 정규표현식에 해당하는 패턴에 해당하는 문자열을 찾을 수 있습니다.

위 그림의 구조를 살펴보면 Context라는 개념과 Expression이라는 개념이 있습니다. Context는 모든 Expression에서 사용되는 공통된 정보로 글로벌한 변수, 값들이 모여있는 곳이고 Expression에서 참조하게 됩니다. Expression의 interpret메서드의 인자로 Context를 받고 있습니다. Expression은 실질적으로 우리가 표현하는 문법을 나타내는데 이 문법에는 2가지가 있습니다. Terminal이 있고 NonTerminal이 있습니다. TerminalExpression같은 경우에는 그 자체로 종료가 되는 expression이고 NonTerminalExpression같은 경우에는 종료되지 않고 다른 Expression들을 재귀적으로 참조하고 있는 구조입니다. 참조하고 있는 다른 Expression들을 다시 interpret해봐야 결과를 알 수 있습니다.

인터프리터 패턴을 적용하기전에 예제 코드를 살펴보면서 문제점을 파악해 보겠습니다. 먼저 PostfixNotaion에서 postfix와 infix를 설명하겠습니다. 보통 우리가 연산을 할때 보통 "1 + 2 - 5" 이런 연산은 infix라고 하는 표현식입니다. 연산자가 피연산자 사이에 들어가 있는 경우를 infix라고 합니다. 연산자가 오른쪽으로 가면 "1 2 + 5 -" postfix라고 합니다. PostfixNotation클래스는 생성자로 postfix표현식을 주입받아서 계산하는 코드입니다.

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());
    }
}

calculate코드를 좀 더 자세하게 살펴보면 postfix표현식을 문자 단위로 잘라서 피연산자면 Stack에 push하고 문자가 '+', '-'면 피연산자 2개를 연달아 pop해서 연산을하고 다시 push해서 연산결과를 출력하는 메서드입니다. 그런데 만약 PostFixNotation생성자에 주입하는 문자열이 "123+-"형태의 "123"대신 다른 숫자로 변경해서 자주 사용한다고 하면 이럴때 인터프리터 패턴을 고려해 볼 수 있습니다. "123+-"문자열을 "xyz+-"문자열이라고 하고 이 자체가 언어 혹은 표현식이라고 할 수 있습니다. 여기에 우리가 원하는 context정보 예를들어 xyz에 숫자정보만 바꿔 넣고싶은 상황입니다.

2. 패턴 적용하기

"123+-"를 일종의 문법으로 만들어서 "xyz+-"로 x, y, z 각각 숫자 값들을 주면 문법에 맞춰서 값들을 대입했을때 어떤 결과가 나오는지 확인해 보겠습니다. 먼저 "xyz+-"를 Expression타입의 Tree구조로 만들기 위해 파싱을 해야 합니다. PostfixParser클래스에 parse메서드를 통해서 PostfixExpression이 생성하도록 정의합니다. 그 다음 interpret을 위해서 Context정보를 줘야하는데 Map.of()를 사용해서 x = 1, y = 2, z = 3의 값들을 각각 정의합니다. 그리고 최종적인 결과값을 출력합니다.

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));
        System.out.println(result);
    }
}

먼저 Expression먼저 정의해보겠습니다. PostfixExpression 인터페이스를 정의하고 Context정보를 Map<Character, Integer>로 하는 interpret추상 메서드를 정의합니다.

import java.util.Map;

public interface PostfixExpression {

    int interpret(Map<Character, Integer> context);

}

TerminalExpression인 VariableExpression을 정의합니다. interpret메서드에서 Character로 받은 값들을 get을 통해서 value값을 리턴하도록합니다. 예를 들어 Character의 값이 x로 들어오면 map에서 x에 해당하는 value를 리턴합니다.

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);
    }
}

VariableExpression보다 좀 더 복잡한 PlusExpression을 정의해보겠습니다. PlusExpression같은경우는 Expression이 left, right 2개가 필요합니다. interpret에서는 left를 interpret한 값과 right를 interpret한 값을 '+' 연산을 통해서 결과값을 리턴하도록 합니다.

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);
    }
}

MinusExpression도 마찬가지 left, right를 interpret해서 '-' 연산을 수행하고 리턴합니다.

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과 Context의 정의는 끝났고 이제 Parser를 구현해보겠습니다. 문자열의 Character를 하나씩 순회하면서 Expression을 생성해서 Stack에 push합니다. getExpression메서드에서 Character가 '+'인 경우 stack에 pop연산을 통해서 2개의 피연산자를 꺼내서 PlusExpression을 생성합니다. '-'인 경우는 먼저 pop한 Expression을 먼저 right로 그 다음 pop한 Expression을 left로 꺼내서 MinusExpression을 생성합니다. 그리고 나머지 Character인 피연산자들이 들어오면 VariableExpression을 생성하도록 합니다.

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을 실행시켜 보겠습니다

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);
    }
}
-4

3. 결론

인터프리터 패턴을 이용하면 자주 등장하는 문제 패턴을 언어와 문법으로 정의할 수 있습니다. 따라서 기존 코드를 변경하지 않고 새로운 Expression을 추가할 수 있습니다. 하지만 복잡한 문법을 표현하게 되면 Expression과 Parser의 코드가 복잡해 진다는 단점이 있습니다.

참고

profile
백엔드 개발자 입니다

0개의 댓글