S E P H ' S

[행동 패턴] 인터프리터(Interpreter) 패턴 본문

Programing & Coding/Design Pattern

[행동 패턴] 인터프리터(Interpreter) 패턴

yoseph0310 2023. 3. 26. 18:31

인터프리터(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 등이 있다.