인터프리터 패턴
주로 프로그래밍 언어의 해석기나 특정 문제를 해결하기 위한 도메인 전용 언어(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();
}
}
장점
- 문법과 해석을 기본 로직에서 분리하여 별도의 클래스로 캡슐화 되므로 모듈화되어 유지보수하기 쉬워진다.
- 문법과 규칙을 계층구조로 명시적으로 모델링하기 때문에 코드의 가독성이 높아진다.
- 문법과 요소의 관계를 이해하는데 도움이 된다.
- 자주 사용하는 문제 패턴(표현식)을 언어와 문법으로 정의할 수 있다.
- 기존 코드를 변경하지 않고 새로운 Expression을 추가할 수 있다.
단점
- 복잡한 문법(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}");
}
}
}
참고자료
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
17. [Design Pattern] 전략(Strategy) 패턴 (0) | 2024.01.17 |
---|---|
16. [Design Pattern] 이터레이터(Iterator) 패턴 (0) | 2024.01.14 |
14. [Design Pattern] 커맨드(Command) 패턴 (0) | 2024.01.09 |
13. [Design Pattern] 책임 연쇄(Chain of Responsibility) 패턴 (1) | 2024.01.06 |
12. [Design Pattern] 프록시(Proxy) 패턴 (1) | 2024.01.06 |