반응형

의도

  • 자주 등장하는 문제를 간단한 언어로 정의하고 재사용하는 패턴.
  • 어떤 언어에 대해, 그 언어의 문법에 대한 표현을 정의하면서 그것(표현)을 사용하여 해당 언어로 기술된 문장을 해석하는 해석자를 함께 정의한다.(Domain Specific Languege, DSL)

 

장점

  • 자주 등장하는 문제 패턴을 언어와 문법으로 정의할 수 있다.
  • 기존 코드를 변경하지 않고 새로운 Expression을 추가할 수 있다.
    • 파서에 해당하는 코드는 변경이 된다. 해당 표현식을 지원해야하기 때문이다.

 

단점

  • 복잡한 문법을 표현하려면 Expression과 Parser가 복잡해진다.

 

알려진 사용 예

  • 자바
    • 자바 컴파일러
    • 정규 표현식
  • 스프링
    • SpEL (스프링 Expression Language)

 

활용성

  • 정의할 언어의 문법이 간단한 경우
    • 문법이 복잡하다면 문법을 정의하는 클래스 계통이 복잡해지고 관리할 수 없게 된다.
    • 문법이 복잡한 경우라면 파서 생성기와 같은 도구를 이용하는 것이 더 나은 방법. 파서 생성기는 추상 구문 트리를 생성하지 않고도 문장을 해석할 수 있기 때문에 시간과 공간을 절약할 수 있다.
  • 효율성은 별로 고려할 사항이 아니다. 사실 가장 효율적인 해석자를 구현하는 방법은 파스 트리를 직접 해석하도록 만드는 것이 아니라, 일차적으로 파스 트리를 다른 형태로 번역(translate)시키는 것이다.
    • 예) 정규 표현식은 일반적으로 유한 상태 기계(finite state machine) 개념으로 번역한다. 이때에도 정규 표현식을 유한 상태 기계로 변형하는 번역기를 구현해야 하는데, 역시 해석자 패턴을 적용할 수 있다.

 

결과

  • 문법의 변경과 확장이 쉽다.
    • 패턴에서 문법에 정의된 규칙을 클래스로 표현하였기 때문에 문법을 변경하거나 확장하려면 상속을 이용하면 된다.
  • 문법의 구현이 용이하다.
    • 추상 구문 트리의 노드에 해당하는 클래스들은 비슷한 구현 방법을 가진다. 이들 클래스를 작성하는 것은 쉬운 일이며, 컴파일러나 파서 생성기를 이용해서 자동 생성할 수도 있다.
      • 추상 구문 트리는 문법을 구조적으로 표현한 트리이다.
  • 복잡한 문법은 관리하기 어렵다.
    • 해석자 패턴은 문법에 정의된 각 규칙별로 적어도 하나의 클래스를 정의한다. 따라서 많은 규칙을 포함하는 문법은 관리 및 유지하기가 어렵다.
  • 표현식을 해석하는 새로운 방법을 추가할 수 있다.
    • 해석자 패턴은 새로운 방식으로 정의된 표현식을 쉽게 해석할 수 있게 해준다.
    • 예) 줄을 잘 맞춘 출력 방식이나 타입 점검을 지원하려면 새로운 연산만 정의하면 된다.

 

협력 방법

  • 사용자는 NonterminalExpression과 TerminalExpression 인스턴스들로 해당 문장에 대한 추상 구문 ㅌ리를 만든다. 그리고 사용자는 Interpret() 연산을 호출하는데, 이때 해석에 필요한 문맥 정보를 초기화한다.
  • 각 NonterminalExpression 노드는 또 다른 서브 표현식에 대한 Interpret()를 이용하여 자신의 Interpret()연산을 정의한다. Interpret()연산은 재귀적으로 Interpret()연산을 이용하여 기본적 처리를 담당한다.
  • 각 노드에 정의한 Interpret()연산은 해석자의 상태를 저장하거나 그것을 알기 위해서 문맥(context) 정보를 이용한다.

 

구조

  • ContextAbstractExpression에서 사용하는 공통된 정보를 의미한다.
    • x = 1, y = 2, z = 3
  • AbstractExpression 은 우리가 실제 표현할 문법을 의미한다.
    • TerminalExpression 은 그 자체로 종료가 되는 Expression
      • x, y, z
    • NonterminalExpression 은 다른 Expression들을 재귀적으로 참조하고 있는 Expression. 그 자체로 종료가 되지 않는다. 참조하고 있는 Expression을 Interpreter로 해석해봐야 결과를 알 수 있다.
      • +, -
        • 다른 Expression 두 개를 Interpreter로 해석한 다음 결과를 더하거나 빼야 한다.

 

실제 구현 구조

 

소스코드

public class App {
    public static void main(String[] args) {
        PostfixExpression expression = PostfixParser.parse("xyz+-");
        int result = expression.interpret(Map.of('x', 1, 'y', 2, 'z', 3));
        System.out.println(result);
    }
}
//AbstractExpression
public interface PostfixExpression {

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

}
//TerminalExpression
public class VariableExpression implements PostfixExpression {

    private final Character variable;

    public VariableExpression(Character variable) {
        this.variable = variable;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return context.get(this.variable);
    }
}
//NonterminalExpression
public class PlusExpression implements PostfixExpression {

    private final PostfixExpression left, 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
public class MinusExpression implements PostfixExpression {

    private final PostfixExpression left, 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 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(); //getExpression 메서드에서 스택에 대한 작업을 모두 수행하고 나면 결국 스택안에 마지막으로 남는 것은 최종 결과이다.
    }

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

 

관련 패턴

  • 추상 구문 트리는 복합체 패턴의 한 인스턴스로 볼 수 있다. 하나의 구문 트리 내에 터미널 기호를 여러 개 공유하기 위해서는 플라이급 패턴을 적용할 수 있다.
  • 해석자는 반복자 패턴을 이용해서 자신의 구조를 순회한다.
  • 방문자 패턴을 이용하면 하나의 클래스에 정의된 구문 트리 각 노드에 대한 상태를 관리할 수 있다.

[참고자료]

리처드 헬름, 랄프 존슨, 존 블리시디스, 『GoF의 디자인 패턴 : 재사용성을 지닌 객체지향 소프트웨어의 핵심요소』, 김정아 번역, 프로텍미디어(2015)

http://www.cs.unc.edu/~stotts/GOF/hires/pat5cfso.htm

코딩으로 학습하는 GoF의 디자인 패턴, 백기선

 

반응형

+ Recent posts