[Design pattern] 인터프리트 패턴(Interpret pattern) (※스택 계산기 예제)


Study/Design Pattern  2019. 6. 10. 23:39

안녕하세요. 명월입니다.


이 글은 인터프리트 패턴(Interpret pattern)에 대한 소개입니다.


개인적인 생각으로 디자인 패턴 중에서 실무에서는 가장 사용하지 않는 패턴이면서 사용하게 되면 가장 괜찮은 패턴이지 않을까 하는 패턴입니다.

사실 인터프리트 패턴은 우리가 가장 자주 사용하는 Sql query문이지 않을 까 싶습니다.

String 식으로 나타내는 표현식을 프로그램 식으로 변환하는게 대표적이지 않을까 싶습니다. 그러나 꼭 Sql query처럼 String 형식이 아닌 Jquery형식이던가 xml 분석식으로도 사용됩니다.

// 계산 기호의 인터페이스
interface IExpression
{
  // 계산 우선 순위에 대한 인터페이스, 즉, + *가 만나면 Rank가 높은 값부터 계산을 함
  // 예로 + , - 는 1, 곱하기 나누기는 2로 작성
  int Rank();
  // 연산 기호의 char 형식을 나타내는 인터페이스
  char OperationKey();
  // 계산 인터페이스
  Decimal Calculate(Decimal a, Decimal b);
}
// 더하기 클래스
class Add : IExpression
{
  // 사칙 연산의 우선 순위는 낮다.
  public int Rank()
  {
    return 1;
  }
  // 연산 기호는 +이다.
  public char OperationKey()
  {
    return '+';
  }
  // 연산은 Decimal 구조체의 Add 함수를 이용한다.
  public Decimal Calculate(Decimal a, Decimal b)
  {
    return Decimal.Add(a, b);
  }
}
// 빼기 클래스
class Subtract : IExpression
{
  // 사칙 연산의 우선 순위는 낮다.
  public int Rank()
  {
    return 1;
  }
  // 연산 기호는 -이다.
  public char OperationKey()
  {
    return '-';
  }
  // 연산은 Decimal 구조체의 Subtract 함수를 이용한다.
  public Decimal Calculate(Decimal a, Decimal b)
  {
    return Decimal.Subtract(a, b);
  }
}
// 곱하기 클래스
class Multiply : IExpression
{
  // 사칙 연산의 우선 순위는 높다
  public int Rank()
  {
    return 2;
  }
  // 연산 기호는 *이다.
  public char OperationKey()
  {
    return '*';
  }
  // 연산은 Decimal 구조체의 Multiply 함수를 이용한다.
  public Decimal Calculate(Decimal a, Decimal b)
  {
    return Decimal.Multiply(a, b);
  }
}
// 나누기 클래스
class Division : IExpression
{
  // 사칙 연산의 우선 순위는 높다
  public int Rank()
  {
    return 2;
  }
  // 연산 기호는 /이다.
  public char OperationKey()
  {
    return '/';
  }
  // 연산은 Decimal 구조체의 Divide 함수를 이용한다.
  public Decimal Calculate(Decimal a, Decimal b)
  {
    return Decimal.Divide(a, b);
  }
}
// 스택 계산기
class Calculator
{
  // 연산 클래스의 리스트
  private List<IExpression> operations = new List<IExpression>();
  // 연산 클래스를 추가한다. (확장을 용이하게)
  public void AddOperation(IExpression op)
  {
    this.operations.Add(op);
  }
  // 외부에서 계산을 호출하기 위한 함수 (expression는 String으로 된 계산식)
  // Facade 패턴으로 구현
  public Decimal Calculate(String expression)
  {
    // String 형식의 표현식을 list형식으로 변환하는 함수 예) 1+2*3 => [1,+,2,*,3] 형식의 리스트 형식
    var list = BuildTokenList(expression);
    // 중위 표기식을 후위 표기식으로 변환
    list = ChangePostfixExpression(list);
    // 계산한다.
    return Result(list);
  }
  // String 형식의 표현식으로 List형식으로 변환하는 함수
  private List<dynamic> BuildTokenList(String expression)
  {
    List<dynamic> ret = new List<dynamic>();
    // 모든 공백은 제거한다.
    expression = expression.Replace(" ", "");
    // 숫자를 위한 버퍼와 처리 함수
    // 예로 두자리 수의 이상의 값이 표현 되었을 경우 String을 합친다. 
    StringBuilder buffer = new StringBuilder();
    Action CreateVariable = () =>
    {
      if (buffer.Length > 0)
      {
        ret.Add(Decimal.Parse(buffer.ToString()));
        buffer.Clear();
      }
    };
    // String의 표현식을 char 배열 형식으로 변환한다.
    foreach (char token in expression.ToCharArray())
    {
      // 등록되어 있는 계산식과 char 문자가 같은게 있는 지 검색한다.
      var op = operations.Where(x => token.Equals(x.OperationKey())).FirstOrDefault();
      // char 문자가 등록 되어있는 연산기호와 같을 경우 그 연산 기호의 클래스를 list에 넣는다.
      // 예로, + - * /의 경우는 if 조건이 참이 되겠다.
      if (op != null)
      {
        // 이전까지 숫자로 등록되어 있던 char를 list에 등록한다.
        CreateVariable();
        // 연산 기호를 등록한다.
        ret.Add(op);
      }
      else
      {
        // 숫자 버퍼에 숫자식을 넣는다. 연달아 숫자가 나타나면 String이 합쳐지는 효과이다.
        buffer.Append(token);
      }
    }
    // 남아있는 숫자를 등록한다.
    CreateVariable();
    return ret;
  }
  // 중위 표기식을 후위 표기식으로 변환한다.
  private List<dynamic> ChangePostfixExpression(List<dynamic> tokenlist)
  {
    var postList = new List<dynamic>();
    var opStack = new Stack<dynamic>();
    // 중위 표기식으로 등록되어 있는 리스트를 Iteration패턴으로 찾는다.
    foreach (var token in tokenlist)
    {
      // 등록 되어 있는 Node값이 숫자일 경우 그대로 등록한다.
      if (typeof(Decimal).Equals(token.GetType()))
      {
        postList.Add(token);
      }
      else
      {
        // 후위 표기식을 만들기 위한 처리
        while (true)
        {
          // 연산 스택에 아무 것도 없으면  연산 스택에 기호를 넣는다. (※후위 표기식에 넣는 것이 아니다)
          if (opStack.Count == 0)
          {
            opStack.Push(token);
            // 그리고 루프 종료
            break;
          }
          // 연산 스택에 한 개 이상 있을 경우 연산 스택 가장 나중에 등록한 기호와 현재의 연산 기호와 비교한다.
          var op1 = opStack.Pop();
          var op2 = token;
          // 만약 현재의 기호가 stack 마지막 기호와 비교시 현재 기호 랭크가 높으면 스택에 이전 것, 그리고 현재 것을 기대로 등록한다.
          // 즉, 1 + 2 * 3 일 경우 +와 *이 만나는데 *의 경우가 Rank가 크기 때문에 Stack에 +, * 순으로 넣는다.
          // 결과는 1 2 3 * +
          if (op1.Rank() <= op2.Rank())
          {
            opStack.Push(op1);
            opStack.Push(op2);
            // 그리고 루프 종료
            break;
          }
          // 만약 현재의 기호가 작을 경우는,
          // 즉, 1 * 2 + 3 일 경우, *와 +가 만나는데, +가 작기 때문에 *는 후위 표기식에 들어간다. 
          // 그리고 루프를 타고 위 if opStack.Count에서 연산 스택에 +가 들어간다.
          // 결과는 1 2 * 3 +
          postList.Add(op1);
        }
      }
    }
    // 연산 스택의 있는 연산식으로 뒤집어서 후위 표기식에 넣는다.
    // 즉 +, * 의 순일 경우 *, + 식으로 등록된다.
    while (opStack.Count != 0)
    {
      postList.Add(opStack.Pop());
    }
    return postList;
  }
  // 후위 표기식으로 되어 있는 계산 식을 계산합니다.
  private Decimal Result(List<dynamic> postList)
  {
    var calcStack = new Stack<dynamic>();
    
    foreach (var item in postList)
    {
      // 숫자면 계산 스택에 넣습니다.
      if (typeof(Decimal).Equals(item.GetType()))
      {
        calcStack.Push(item);
        continue;
      }
      IExpression op = item;
      // 후위 표기식의 의해 연산식이 나오면 당연히 숫자 두개가 calcStack에 있습니다.
      var op1 = calcStack.Pop();
      var op2 = calcStack.Pop();
      // 해당 연산 클래스의 Calculate함수를 통해 계산합니다.
      var result = op.Calculate(op1, op2);
      // 그것을 다시 스택에 넣습니다.
      calcStack.Push(result);
    }
    // 디버그 예제 (1 + 2 * 3)
    // 1 2 3 * + (후위 표기식)
    // if * 일 경우 
    // [1,2,3] => [1,6]
    // if + 일 경우
    // [1,6] => [7]
    return calcStack.Pop();
  }
}
class Program
{
  static void Main(string[] args)
  {
    var calc = new Calculator();
    calc.AddOperation(new Add());
    calc.AddOperation(new Subtract());
    calc.AddOperation(new Multiply());
    calc.AddOperation(new Division());
    //result 7
    Console.WriteLine(calc.Calculate("1 + 2 * 3"));
    //Result 5;
    Console.WriteLine(calc.Calculate("1 * 2 + 3"));
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 예제는 스택 계산기를 인터프리트 패턴 형식으로 작성한 예제입니다.

제가 디자인 패턴에 대해 100% 이해하기 전에 작성한 소스와 비교하면 디자인 패턴이 얼마나 괜찮은 가독성을 가지고 있는지 알 수있습니다.

[C#] 계산기 프로그램 - https://nowonbun.tistory.com/319

예전에 작성했한 스택 계산기는 꽤나 복잡합니다. 사실 위 예제는 사칙 연산만 들어가 있고 예전에 작성한 것은 더 많은 계산식이 있습니다.

예전 소스는 하나의 클래스 안에서 함수를 여러개 만들어서 처리하는 형식이여서 클래스 구조가 꽤나 복잡하고 어렵습니다. 그러나 디자인 패턴을 적용한 위 계산식은 IExpress 인터페이스를 상속해서 작성하고 Calc클래스안에서 처리하는 방식으로 만들어서 보기에도 깔끔하고 나중에 수정 또는 확장이 엄청 편해 보입니다.

사실 이게 디자인 패턴의 힘이지 않을까 생각됩니다.


여기까지 인터프리트 패턴에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.