Interpreter
Interpreter Pattern이란?
인터프리터 패턴Interpreter Pattern은 주어진 언어의 문법을 정의하고, 해당 언어로 작성된 문장을 해석하는 방법을 제공하는 행동 패턴입니다. 이 패턴은 간단한 언어의 구문 분석기나 도메인 특화 언어DSL를 구축할 때 유용하게 사용됩니다. 이 패턴을 통해 언어의 문법을 클래스 구조로 표현하고, 표현식을 해석하는 코드를 체계적으로 구성할 수 있습니다.
Interpreter Pattern의 필요성
다음과 같은 상황에서 인터프리터 패턴을 사용할 수 있습니다:
간단한 언어 구현
인터프리터 패턴은 도메인 특정 언어DSL 또는 간단한 스크립팅 언어를 구현할 때 유용합니다. 문법 규칙을 정의하고, 그 규칙을 기반으로 주어진 문장을 해석할 수 있습니다.
반복적인 해석 작업
동일한 문법을 반복적으로 해석해야 하는 작업에서, 문법 규칙을 캡슐화하여 일관된 해석 로직을 구현할 수 있습니다.
Interpreter Pattern 구조
- Client → Context
- 클라이언트는 해석할 입력을
Context
에 전달합니다. Context
는 입력 데이터와 해석 상태를 저장합니다.
- 클라이언트는 해석할 입력을
- Client → AbstractExpression
- 클라이언트는
AbstractExpression
인터페이스를 통해 해석 작업을 시작합니다.
- 클라이언트는
- AbstractExpression
- 해석의 기본 인터페이스를 제공합니다.
TerminalExpression
및NonTerminalExpression
이 이를 구현합니다.
- TerminalExpression
- 기본 요소(예: 숫자, 변수 등)에 대한 해석을 처리합니다.
- NonTerminalExpression
- 복합 요소(예: 연산자, 구조 등)에 대한 해석을 처리합니다.
- 내부적으로 다른
AbstractExpression
을 재귀적으로 호출합니다.
- NonTerminalExpression → AbstractExpression
- NonTerminalExpression이 재귀적으로 다른 표현식을 해석합니다.
- AbstractExpression → Context
- 각
AbstractExpression
은 해석 결과를Context
에 저장합니다. Context
는 해석 과정에서 공유되는 상태뿐만 아니라, 최종 출력 데이터도 유지합니다.
- 각
Interpreter Pattern의 구성 요소
추상 표현식AbstractExpression
문법 규칙을 표현하는 인터페이스나 추상 클래스입니다. 이 클래스는 Interpret
메서드를 정의하여 문장을 해석하는 기능을 제공합니다.
터미널 표현식TerminalExpression
문법에서 더 이상 분해할 수 없는 기본 단위인 터미널 기호를 해석하는 클래스입니다.
비터미널 표현식NonTerminalExpression
터미널 표현식을 포함한 더 복잡한 문법 규칙을 해석하는 클래스입니다.
문맥Context
문법을 해석하는 데 필요한 정보나 환경을 제공하는 클래스입니다.
클라이언트Client
클라이언트는 문법을 구성하고, 표현식들을 조합하여 문장을 해석하는 역할을 합니다.
Interpreter Pattern 적용
잘못된 방식의 해석 처리
인터프리터 패턴을 적용하지 않은 경우, 문장을 해석할 때 조건문이나 반복문을 사용하여 각 문법 규칙을 수동으로 처리하게 됩니다. 이는 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.
public class Calculator
{
public int Evaluate(string expression)
{
string[] tokens = expression.Split(' ');
int left = int.Parse(tokens[0]);
string operation = tokens[1];
int right = int.Parse(tokens[2]);
switch (operation)
{
case "+":
return left + right;
case "-":
return left - right;
default:
throw new InvalidOperationException("Unsupported operation");
}
}
}
문법 확장 어려움
위 코드는 새로운 연산을 추가하려면 Evaluate
메서드를 수정해야 하며, 문법이 복잡해질수록 코드가 복잡해집니다.
재사용성 부족
각 문법 규칙이 개별적으로 캡슐화되어 있지 않아, 재사용하거나 수정하기 어렵습니다.
Interpreter Pattern 적용 예시
// 추상 표현식 클래스
public abstract class Expression
{
public abstract int Interpret();
}
// 터미널 표현식: 숫자 해석
public class NumberExpression : Expression
{
private int _number;
public NumberExpression(int number)
{
_number = number;
}
public override int Interpret() => _number;
}
// 비터미널 표현식: 덧셈 해석
public class AddExpression : Expression
{
private Expression _leftExpression;
private Expression _rightExpression;
public AddExpression(Expression left, Expression right)
{
_leftExpression = left;
_rightExpression = right;
}
public override int Interpret() => _leftExpression.Interpret() + _rightExpression.Interpret();
}
// 비터미널 표현식: 뺄셈 해석
public class SubtractExpression : Expression
{
private Expression _leftExpression;
private Expression _rightExpression;
public SubtractExpression(Expression left, Expression right)
{
_leftExpression = left;
_rightExpression = right;
}
public override int Interpret() => _leftExpression.Interpret() - _rightExpression.Interpret();
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 5 + 10 - 3 의 해석
Expression expr = new SubtractExpression(
new AddExpression(new NumberExpression(5), new NumberExpression(10)),
new NumberExpression(3)
);
int result = expr.Interpret();
Console.WriteLine($"Result: {result}"); // 출력: Result: 12
}
}
NumberExpression 클래스
숫자를 표현하는 터미널 표현식입니다. 주어진 숫자를 반환합니다.
AddExpression, SubtractExpression 클래스
덧셈과 뺄셈을 수행하는 비터미널 표현식입니다. 두 표현식을 받아서 각각의 해석 결과를 더하거나 뺍니다.
클라이언트 코드
숫자와 연산자를 조합하여 5 + 10 - 3의 계산을 해석하고, 결과를 출력합니다.
Interpreter Pattern 장단점
장점
문법의 캡슐화
문법 규칙을 각각의 클래스나 객체로 캡슐화할 수 있으므로, 새로운 문법 규칙을 추가하거나 수정할 때 다른 코드에 영향을 미치지 않습니다.
확장성
새로운 문법 규칙을 추가하기가 쉬워집니다. 새로운 표현식을 추가하려면 해당하는 클래스를 작성하여 문법을 확장할 수 있습니다.
유지보수 용이성
문법이 복잡해질수록 코드가 더 체계적으로 관리될 수 있어, 유지보수성이 향상됩니다.
단점
성능 문제
복잡한 문법 구조를 처리할 때, 많은 객체가 생성되고 호출되기 때문에 성능이 저하될 수 있습니다. 특히, 표현식의 깊이가 깊어지면 성능 저하가 심해질 수 있습니다.
복잡한 구조
문법 규칙이 많아질수록 클래스를 많이 작성해야 하므로, 코드 구조가 복잡해질 수 있습니다.
단점 해결 방안
캐싱을 통한 성능 최적화
반복적으로 해석되는 표현식의 결과를 캐싱하여, 불필요한 연산을 줄일 수 있습니다.
문법 간소화
필요하지 않은 복잡한 문법 규칙을 제거하고, 가능한 한 간단한 문법으로 표현식을 구성하여 코드의 복잡성을 줄일 수 있습니다.
객체지향 원칙과의 관계
Interpreter와 다형성
인터프리터 패턴은 다형성을 적극 활용하여, 표현식을 다양한 형태로 처리할 수 있도록 합니다. 모든 표현식은 공통 인터페이스를 통해 동일하게 처리됩니다.
Interpreter와 단일 책임 원칙
각 표현식 클래스는 하나의 문법 규칙을 처리하는 책임만을 가지므로, 단일 책임 원칙을 잘 준수합니다.
Interpreter와 개방_폐쇄 원칙
새로운 문법 규칙을 추가할 때 기존 코드를 수정할 필요가 없으므로, 개방-폐쇄 원칙을 충족합니다.
결론
인터프리터 패턴은 간단한 언어를 해석하거나 도메인 특화 언어를 구축할 때 매우 유용한 패턴입니다. 문법 규칙을 캡슐화하고 체계적으로 관리할 수 있으며, 새로운 문법을 쉽게 추가할 수 있는 장점이 있습니다. 다만, 복잡한 문법을 처리할 때 성능 문제가 발생할 수 있으므로, 성능 최적화를 고려하는 것이 중요합니다.
심화학습
추상 구문 트리와의 연관성
인터프리터 패턴은 추상 구문 트리Abstract Syntax Tree, AST와 자주 함께 사용됩니다. AST는 문법의 구조를 트리 형태로 표현하며, 각 노드는 특정 구문 요소를 나타냅니다. 인터프리터 패턴을 사용하면, 각 구문 요소를 객체로 표현할 수 있고, 이 객체들이 모여 구문 트리를 구성합니다. 이는 컴파일러나 인터프리터가 소스 코드를 분석하고 실행하는 데 사용되는 중요한 개념입니다.
예시: 간단한 산술 계산기
// 추가: 곱셈 표현식
public class MultiplyExpression : Expression
{
private Expression _leftExpression;
private Expression _rightExpression;
public MultiplyExpression(Expression left, Expression right)
{
_leftExpression = left;
_rightExpression = right;
}
public override int Interpret() => _leftExpression.Interpret() * _rightExpression.Interpret();
}
// 사용 예시
Expression expr = new MultiplyExpression(
new AddExpression(new NumberExpression(3), new NumberExpression(5)),
new SubtractExpression(new NumberExpression(10), new NumberExpression(7))
);
int result = expr.Interpret();
Console.WriteLine($"Result: {result}"); // 출력: Result: 24
위 예시는 곱셈과 같은 복잡한 연산이 추가된 구문 트리 형태의 수식을 보여줍니다. 이처럼, AST를 구성하여 보다 복잡한 계산이나 문법을 처리할 수 있습니다.
성능 최적화 방안: 캐싱
인터프리터 패턴은 각 표현식을 해석할 때마다 객체를 생성하고, 해석하는 과정에서 많은 자원을 소모할 수 있습니다. 이를 해결하기 위해 캐싱 기법을 사용할 수 있습니다. 동일한 표현식에 대해 여러 번 해석하는 경우, 해석된 결과를 저장해두고, 반복 호출 시 캐싱된 결과를 반환하도록 할 수 있습니다.
public class CachedExpression : Expression
{
private Expression _expression;
private int? _cachedResult;
public CachedExpression(Expression expression)
{
_expression = expression;
}
public override int Interpret()
{
if (_cachedResult == null)
{
_cachedResult = _expression.Interpret();
}
return _cachedResult.Value;
}
}
// 사용 예시
Expression expr = new CachedExpression(
new MultiplyExpression(new NumberExpression(3), new NumberExpression(5))
);
int result1 = expr.Interpret(); // 계산 수행
int result2 = expr.Interpret(); // 캐싱된 결과 반환
위 코드에서 CachedExpression
클래스는 Interpret()
메서드가 한 번 계산된 결과를 캐싱하여, 이후 호출 시 캐싱된 값을 반환합니다. 이는 동일한 계산을 여러 번 반복해야 할 때 성능을 크게 향상시킬 수 있습니다.
복잡한 문법 해석
인터프리터 패턴을 복잡한 언어 해석에 사용하려면, 다음과 같은 추가적인 기법을 적용할 수 있습니다.
우선순위 처리
산술 표현식에서 연산자 우선순위를 처리해야 한다면, 각 연산자의 우선순위를 고려하여 구문 트리를 생성하는 과정이 필요합니다. 예를 들어, 곱셈과 나눗셈은 덧셈과 뺄셈보다 우선 실행되도록 트리 구조를 만들어야 합니다.
변수와 상태 추가
간단한 표현식을 넘어, 변수와 변수를 참조하는 구문도 해석할 수 있습니다. 이를 위해서는 변수 테이블이나 상태 저장 객체를 추가하여 변수를 관리하는 로직을 포함시켜야 합니다.
// 변수 저장 및 참조 클래스
public class VariableExpression : Expression
{
private string _name;
private Dictionary<string, int> _variables;
public VariableExpression(string name, Dictionary<string, int> variables)
{
_name = name;
_variables = variables;
}
public override int Interpret()
{
if (_variables.ContainsKey(_name))
{
return _variables[_name];
}
throw new Exception($"Undefined variable: {_name}");
}
}
// 클라이언트 코드
var variables = new Dictionary<string, int> { { "x", 10 }, { "y", 5 } };
Expression expr = new AddExpression(
new VariableExpression("x", variables),
new VariableExpression("y", variables)
);
int result = expr.Interpret();
Console.WriteLine($"Result: {result}"); // 출력: Result: 15
위 예시에서는 변수를 지원하는 VariableExpression
클래스를 추가하여, 계산식에서 변수 값을 참조할 수 있게 했습니다. 이 방식으로 문법을 확장할 수 있습니다.
Interpreter와 Abstract Factory
복잡한 언어나 구문 해석기를 개발할 때, 추상 팩토리Abstract Factory 패턴과 결합하여 표현식 객체의 생성을 캡슐화할 수 있습니다. 이를 통해 클라이언트가 각 표현식의 세부 구현을 알 필요 없이, 인터프리터를 유연하게 확장하고 관리할 수 있습니다.
// 추상 팩토리
public abstract class ExpressionFactory
{
public abstract Expression CreateNumber(int number);
public abstract Expression CreateAdd(Expression left, Expression right);
public abstract Expression CreateSubtract(Expression left, Expression right);
}
// 구체적인 팩토리
public class ConcreteExpressionFactory : ExpressionFactory
{
public override Expression CreateNumber(int number) => new NumberExpression(number);
public override Expression CreateAdd(Expression left, Expression right)
=> new AddExpression(left, right);
public override Expression CreateSubtract(Expression left, Expression right)
=> new SubtractExpression(left, right);
}
// 클라이언트 코드
ExpressionFactory factory = new ConcreteExpressionFactory();
Expression expr = factory.CreateAdd(factory.CreateNumber(10), factory.CreateSubtract(factory.CreateNumber(5), factory.CreateNumber(3)));
int result = expr.Interpret();
Console.WriteLine($"Result: {result}"); // 출력: Result: 12
추상 팩토리를 사용하면, 클라이언트는 표현식 객체를 직접 생성하지 않고 팩토리를 통해 생성하며, 이는 시스템의 유연성을 높입니다.
Interpreter와 쿼리
인터프리터 패턴은 도메인 특화 언어DSL나 간단한 규칙 해석에서 매우 유용합니다. 예를 들어, 도서 관리 시스템에서 특정 검색 쿼리나 규칙을 해석하는 데 사용할 수 있습니다. 사용자는 "author == 'J.K. Rowling' AND year > 2000"
과 같은 쿼리를 입력하고, 이를 해석하여 도서 목록을 필터링할 수 있습니다.
// 검색 조건 인터페이스
public abstract class SearchExpression
{
public abstract bool Interpret(Book book);
}
// 터미널 표현식: 저자 검색
public class AuthorExpression : SearchExpression
{
private string _author;
public AuthorExpression(string author)
{
_author = author;
}
public override bool Interpret(Book book) => book.Author == _author;
}
// 터미널 표현식: 출판 연도 검색
public class YearExpression : SearchExpression
{
private int _year;
public YearExpression(int year)
{
_year = year;
}
public override bool Interpret(Book book) => book.Year > _year;
}
// 비터미널 표현식: AND 연산
public class AndExpression : SearchExpression
{
private SearchExpression _leftExpression;
private SearchExpression _rightExpression;
public AndExpression(SearchExpression left, SearchExpression right)
{
_leftExpression = left;
_rightExpression = right;
}
public override bool Interpret(Book book)
=> _leftExpression.Interpret(book) && _rightExpression.Interpret(book);
}
// Book 클래스
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
var books = new List<Book>
{
new Book { Title = "Harry Potter", Author = "J.K. Rowling", Year = 2001 },
new Book { Title = "The Hobbit", Author = "J.R.R. Tolkien", Year = 1937 },
};
SearchExpression query = new AndExpression(
new AuthorExpression("J.K. Rowling"),
new YearExpression(2000)
);
foreach (var book in books)
{
if (query.Interpret(book))
{
Console.WriteLine($"Found: {book.Title}");
}
}
}
}
위 예시에서는 도서 관리 시스템에서 “저자가 ‘J.K. Rowling’이고, 출판 연도가 2000년 이후인 도서"를 검색하는 규칙을 해석하는 인터프리터 패턴을 적용했습니다. 이 방식은 복잡한 검색 규칙을 캡슐화하여 처리할 수 있게 해줍니다.
Interpreter와 Decorator
데코레이터 패턴과 결합하여 표현식에 추가적인 기능을 동적으로 부여할 수 있습니다. 예를 들어, 결과를 캐싱하거나 로그를 남기는 등의 기능을 동적으로 추가할 수 있습니다.
// 데코레이터 클래스
public class LoggingExpressionDecorator : Expression
{
private Expression _expression;
public LoggingExpressionDecorator(Expression expression)
{
_expression = expression;
}
public override int Interpret()
{
int result = _expression.Interpret();
Console.WriteLine($"Interpreted result: {result}");
return result;
}
}
// 클라이언트 코드
Expression expr = new LoggingExpressionDecorator(
new AddExpression(new NumberExpression(5), new NumberExpression(10))
);
int result = expr.Interpret(); // 로그와 함께 해석