프로그래밍 이론/디자인 패턴

15. [Design Pattern] 인터프리터(interpreter) 패턴

NewtronVania 2024. 1. 14. 00:26

인터프리터 패턴

주로 프로그래밍 언어의 해석기나 특정 문제를 해결하기 위한 도메인 전용 언어(Domain Specific Language, DSL) 구현할 사용되는 디자인 패턴. 언어의 문법 규칙을 클래스로 표현하고, 해당 클래스의 인스턴스를 사용하여 문장을 해석하는 방식을 제공한다.


구조

  • Expression -  트리에 속한 모든 노드에 해당하는 클래스들이 공통으로 가져야 할 interpret 연산을 추상 연산으로 정의한다.
  • TermianlExpression - 문법의 기본 요소를 해석하는 방법을 구현한다. 문장을 구성하는 모든 기호에 대해서 해당 클래스를 만들어야 한다. 
  • NonterminalExpression - 문법의 규칙을 구성하는 역할을 한다. 
  • Context - interpret 연산에 의해 전달되는 정보를 포함한다. 이 정보는 특정 표현식이 해석되는 데 필요한 정보를 포함한다.
  • Client - 인터프리터 패턴을 사용하는 클래스다. 언어로 정의한 특정 문장을 나타내는 추상 구문 트리를 Expression 구현 클래스에 전달하고 그 결과를 반환받는다.

기본 인터페이스

// AbstractExpression
public interface IExpression
{
    int Interpret(Context context);
}

// TerminalExpression
public class NumberExpression : IExpression
{
    private int number;

    public NumberExpression(int number)
    {
        this.number = number;
    }

    public int Interpret(Context context)
    {
        return number;
    }
}

// NonterminalExpression
public class AddExpression : IExpression
{
    private IExpression leftExpression;
    private IExpression rightExpression;

    public AddExpression(IExpression left, IExpression right)
    {
        leftExpression = left;
        rightExpression = right;
    }

    public int Interpret(Context context)
    {
        return leftExpression.Interpret(context) + rightExpression.Interpret(context);
    }
}

// Context (예시를 단순화하기 위해 여기서는 빈 클래스로 둡니다)
public class Context
{
}

// Client
class Program
{
    static void Main(string[] args)
    {
        IExpression left = new NumberExpression(1);
        IExpression right = new NumberExpression(2);
        IExpression expression = new AddExpression(left, right);

        Context context = new Context();
        int result = expression.Interpret(context);

        Console.WriteLine("Result: " + result);
    }
}

왜 사용해야 할까?

인터프리터 패턴은 복잡한 문법 규칙을 가진 언어를 해석해야 유용하다. 패턴을 사용하면 문법 규칙을 클래스로 분리하여 관리할 있어, 문법의 해석 과정을 명확하고 유지 관리가 쉬운 구조로 만들 있다. 또한, 새로운 규칙을 추가하거나 기존 규칙을 변경하는 것이 비교적 간단해진다.

인터프리터 패턴을 이용하면 간단한 산술 연산을 해석하는 인터프리터(해석기) 구현할 있으며, 이는 새로운 연산자를 추가하거나 기존 연산자의 동작을 쉽게 변경할 있다. 이런 방식으로, 인터프리터 패턴은 소프트웨어가 동적으로 특정 언어의 문장을 해석하고 실행할 있게 해준다.

예시

계산기

입력받은 순서대로 사칙연산을 하여 결과를 출력하는 계산기 프로그램을 작성하겠다. 

IExpression은 IExpression 구현 클래스들이 가져야 할 Interpret 메서드를 정의한다. IExpression를 상속받은 클래스들은 문장을 통해 각자의 해석방식을 구현하고 그 값을 반환한다.

// AbstractExpression
public interface IExpression
{
    int Interpret();
}

TerminalExpression은 문법의 기본요소를 표현한다. 문법 내에서 더 이상 분리될 수 없는 가장 작은 최소 단위를 표현하는 규칙을 설명한다. 이 계산기 프로그램에서는 간단산 사칙연산을 규칙으로 하고 있고, 그 안의 최소 단위는 각 숫자로 생각할 수 있다. 

 

// TerminalExpression
public class NumberExpression : IExpression
{
    private int number;

    public NumberExpression(int number)
    {
        this.number = number;
    }
    //char로 입력받을 경우 int 값으로 변환한다.
    public NumberExpression(char c)
    {
        this.number = c - '0';
    }

    public int Interpret()
    {
        return number;
    }
}

NontermianlExpression은 문법의 복합적인 규칙을 표현한다. 다른 터미널(IExpression)들의 표현식을 조합하여 더 복잡한 구조를 형성하는 것을 목표하는 클래스다. 이 프로그램에서는 간단한 사칙연산을 표현하는 클래스들이 담당한다. 사칙연산은 두 값(터미널)을 조합하여 그 결과값을 반환하기 때문에 NontermianlExpression에 적합하다.

// NonterminalExpression for Addition
// 문법의 비터미널 표현식 중 하나. 여기서는 덧셈을 담당한다.
public class PlusExpression : IExpression
{
    private IExpression leftExpression;
    private IExpression rightExpression;

    public PlusExpression(IExpression left, IExpression right)
    {
        leftExpression = left;
        rightExpression = right;
    }

    public int Interpret()
    {
        return leftExpression.Interpret() + rightExpression.Interpret();
    }
}

// 뺄셈, 곱셈, 나눗셈에 대한 비터미널 표현식 구현
// ...

이제 계산기 Calcuator를 구현할 차례이다. Calcuator는  위 표현식을 통해 입력받은 context를 해석하는 클래스다. 입력받은 사칙연산 context를 숫자(Terminal)과 사칙연산(Nonterminal)로 분리하고 해석하여 그 결과값을 Client에게 반환하는 클래스다. Client는 입력해야 할 context 규칙만을 알면 되고 해석 방식과 내부는 캡슐화되어 그 결과값만을 받는다.

public class Calculator{
    // Calculate 메서드는 주어진 문자열 표현을 해석하고 결과를 반환한다.
    // 인터프리터 패턴을 사용하여 산술 표현식을 평가한다.
    public int Calculate(String context)
    {
        IExpression expression = getExpression(context);
        int result = expression.Interpret();
        return result;
    }

    // getExpression 메서드는 문자열 표현에서 추상 구문 트리를 구성한다.
    // 문법 규칙에 따라 스택에 터미널 및 비터미널 표현식을 푸시한다.
    private IExpression getExpression(string context)
    {
        Stack<IExpression> stack = new Stack<IExpression>();
        for (int i = 0; i < context.Length; i++)
        {
            char c = context[i];
            switch (c) 
            {
                // 연산자에 따라 비터미널 표현식을 생성하는 작업
                // ...
                default:
                    stack.Push(new NumberExpression(c));
                    break;
            }
        }
        return stack.Pop();
    }
}

 

장점

  1. 문법과 해석을 기본 로직에서 분리하여 별도의 클래스로 캡슐화 되므로 모듈화되어 유지보수하기 쉬워진다.
  2. 문법과 규칙을 계층구조로 명시적으로 모델링하기 때문에 코드의 가독성이 높아진다.
  3. 문법과 요소의 관계를 이해하는데 도움이 된다.
  4. 자주 사용하는 문제 패턴(표현식)을 언어와 문법으로 정의할 수 있다.
  5. 기존 코드를 변경하지 않고 새로운 Expression을 추가할 수 있다.

단점

  1. 복잡한 문법(Context)를 표현하려면 그만큼 Expression과 Parser가 복잡해진다.

전체 예시 코드

namespace Interpreter;

// AbstractExpression
public interface IExpression
{
    int Interpret();
}

// TerminalExpression
public class NumberExpression : IExpression
{
    private int number;

    public NumberExpression(int number)
    {
        this.number = number;
    }

    public NumberExpression(char c)
    {
        this.number = c - '0';
    }

    public int Interpret()
    {
        return number;
    }
}

// NonterminalExpression for Addition
public class PlusExpression : IExpression
{
    private IExpression leftExpression;
    private IExpression rightExpression;

    public PlusExpression(IExpression left, IExpression right)
    {
        leftExpression = left;
        rightExpression = right;
    }

    public int Interpret()
    {
        Console.WriteLine($"{leftExpression.Interpret()} + {rightExpression.Interpret()} = {leftExpression.Interpret() + rightExpression.Interpret()}");
        return leftExpression.Interpret() + rightExpression.Interpret();
    }
}




public class MinusExpression : IExpression
{
    private IExpression leftExpression;
    private IExpression rightExpression;

    public MinusExpression(IExpression left, IExpression right)
    {
        leftExpression = left;
        rightExpression = right;
    }

    public int Interpret()
    {
        return leftExpression.Interpret() - rightExpression.Interpret();
    }
}


public class MulExpression : IExpression
{
    private IExpression leftExpression;
    private IExpression rightExpression;

    public MulExpression(IExpression left, IExpression right)
    {
        leftExpression = left;
        rightExpression = right;
    }

    public int Interpret()
    {
        return leftExpression.Interpret() * rightExpression.Interpret();
    }
}

public class DivideExpression : IExpression
{
    private IExpression leftExpression;
    private IExpression rightExpression;

    public DivideExpression(IExpression left, IExpression right)
    {
        leftExpression = left;
        rightExpression = right;
    }

    public int Interpret()
    {
        return leftExpression.Interpret() / rightExpression.Interpret();
    }
}


public class Calculator{
    public int Calculate(String context)
    {
        IExpression expression = getExpression(context);
        int result = expression.Interpret();
        return result;
    }

    private IExpression getExpression(string context)
    {
        Stack<IExpression> stack = new Stack<IExpression>();
        for (int i = 0; i < context.Length; i++)
        {
            char c = context[i];
            switch (c) 
            {
                case '+':
                    stack.Push(new PlusExpression(stack.Pop(), new NumberExpression(context[++i])));
                    break;
                case '-':
                    stack.Push(new MinusExpression(stack.Pop(), new NumberExpression(context[++i])));
                    break;
                case '*':
                    stack.Push(new MulExpression(stack.Pop(), new NumberExpression(context[++i])));
                    break;
                case '/':
                    stack.Push(new DivideExpression(stack.Pop(), new NumberExpression(context[++i])));
                    break;
                default:
                    stack.Push(new NumberExpression(c));
                    break;
            }
        }
        return stack.Pop();
    }
}

class Program
{
    static void Main(string[] args)
    {
        Calculator calculator = new Calculator();
        while (true)
        {
            string expression = Console.ReadLine();

            if (expression.Length <= 0)
            {
                Console.WriteLine("Calculator is End!");
                break;
            }

            int result = calculator.Calculate(expression);
            Console.WriteLine($"Result : {result}");
        }
    }
}

참고자료

 

GoF의 디자인 패턴 - 예스24

이 책은 디자인 패턴을 다룬 이론서로 디자인 패턴의 기초적이고 전반적인 내용을 학습할 수 있다.

www.yes24.com