본문 바로가기
3학년 2학기 학사/소프트웨어 분석 및 설계

[소프트웨어 분석 및 설계] L21. Interpreter Pattern, Iterator Pattern

by whiteTommy 2024. 12. 13.
반응형

Interpreter Pattern

  • 해석자 (interpreter) 패턴이란?
    • 언어(language) 의 문법 규칙을 표현하고 해당 언어의 해석을 하는데 사용되는 행동 패턴
      • 컴파일러 또는 인터프리터를 개발할 때 적용되며 언어의 문법 구조를 나타내는데 유용
      • 문법 규칙을 클래스로 표현하고, 이러한 클래스들을 조합하여 언어의 문장을 해석하는 구조를 만듬
      • 새로운 언어를 추가하거나 기존 언어의 문법을 변경할 때 유연하게 확장 가능!
  • 해석자 패턴을 도입하기 위한 예제 - Postfix expression
    • Infix notation
      • 일반적인 표기법, 사람들이 보기에 익수함
      • 연산자 우선순위 때문에 괄호가 필요함 (우선순위 처리 복잡)
    • Postfix notation
      • 사람이 보기에 직관적이지 않지만, 수식을 읽으면서 바로 계산이 가능
        • 수신에 괄호가 필요 없음
        • 수식 자체에 연산자의 우선순위가 포함되어 표현됨
    • 특정 연산자는 가장 마지막으로 등장한 두 피연산자에 대해 계산
    • 가장 마지막으로 등장한 피연산자 (결과값)을 파악하기 위해 스택을 활용!
      public class PostfixNotation {
          private final String expression;
      
          public PostfixNotation(String expression) {
              this.expression = expression;
          }
          
          public static void main(String[] args){
          	PostfixNotation postfixNotation = new PostfixNotation("123+-");
              postfixNotation.calculate();
          }
      
          public void calculate() {
              Stack<Integer> numbers = new Stack<>();
              
              for (char c : expression.toCharArray()) {
                  switch(c){
                  	case '+':
                      	numbers.push(numbers.pop() + numbers.pop());
                          break;
                      case '-':
                      	int right = numbers.pop();
                          int left = numbers.pop();
                          numbers.push(left-right);
                          break;
                      default:
                      	numbers.push(Integer.parseInt(c+""));
                  }
              }
              System.out.println(numbers.pop()); // 최종 결과 출력
          }
      }


  • 해석자 패턴의 구조
    • 특정 행동들을 핸들러라는 독립 실행형 객체로 변환
  • 책임자 패턴 예시 코드 (Terminal expression)
    public class App {
        public static void main(String[] args) {
            PostfixExpression expression = PostfixParser.parse("xyz+-a");
            Map<Character, Integer> context = new HashMap<>();
            context.put('x', 2);
            context.put('y', 3);
            context.put('z', 1);
            context.put('a', 4);
    
            int result = expression.interpret(context); // 표현식 해석
            System.out.println(result); // 결과 출력
        }
    }
    
    public interface PostfixExpression {
        int interpret(Map<Character, Integer> context);
    }
    
    public class VariableExpression implements PostfixExpression {
        private Character character;
    
        public VariableExpression(Character character) {
            this.character = character;
        }
    
        @Override
        public int interpret(Map<Character, Integer> context) {
            return context.get(this.character); // 변수의 값 반환
        }
    }
  • 책임자 패턴 예시 코드 (Nonterminal expression)
    public class PlusExpression implements PostfixExpression {
        private 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); // 좌 + 우
        }
    }
    
    public class MinusExpression implements PostfixExpression {
        private 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();
        }
    
        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); // 변수 처리
            }
        }
    }


  • 장점
    • 새로운 언어 요소나 문법 규칙을 추가하기가 유연함
    • 단순한 문법을 가지는 언어의 경우 인터프리터 패턴이 효과적
    • 언어의 문법이나 규칙이 변경되더라도 인터프리터 패턴은 변화에 대응이 쉬움
  • 단점
    • 언어의 문법이 복잡한 경우 인터프리터 패턴의 클래스 계층 구조가 복잡해짐
    • 일부 복잡한 언어나 대규모 문장에 대해서는 패턴 적용 시 성능이 저하될 수 있음

 

 

Iterator Pattern

  • 반복자 (iterator) 패턴이란?
    • 컬렉션(collection) 요소들의 기본 표현 (리스트, 스택, 트리 등)을 노출하지 않고 컬렉션 내 요소를 하나씩 순회할 수 있도록 하는 행동 패턴
  • 반복자 패턴이 필요한 상황
    • 데이터 컬렉션이란 객체들을 그룹으로 묶어 자료구조에 맞게 저장함
    • 선형적인 자료구조 (배열, 리스트 등) 는 순차적으로 요소 조회를 할 수 있지만, 트리, 해시와 같은 비선형적인 자료구조는 순회의 기준이 필요함
    • 클라이언트가 컬렉션에 종류를 따지지 않고 순회할 수 있는 방법이 있을까?
  • 반복자 패턴의 아이디어
    • 컬렉션의 순회 동작을 iterator 라는 별도 객체로 추출! 
  • 반복자 패턴의 구조
  • 반복자 패턴 코드 예시
    public interface IterableCollection {
        Iterator iterator(); // Iterator 반환
    }
    
    public class ArrayCollection implements IterableCollection {
        Object[] array;
        int index = 0; // 현재 요소 추가 위치
    
        public ArrayCollection(int size) {
            this.array = new Object[size]; // 지정된 크기의 배열 생성
        }
    
        public void append(Object obj) {
            if (index < array.length) {
                array[index++] = obj; // 배열에 요소 추가
            }
        }
    
        @Override
        public Iterator iterator() {
            return new ConcreteIterator(array); // ConcreteIterator 생성
        }
    }
    
    public interface Iterator {
        boolean hasNext(); // 다음 요소가 있는지 확인
        Object next();     // 다음 요소 반환
    }
    
    public class ConcreteIterator implements Iterator {
        Object[] array;
        private int nextIndex = 0; // 현재 순회 위치
    
        public ConcreteIterator(Object[] array) {
            this.array = array;
        }
    
        @Override
        public boolean hasNext() {
            return nextIndex < array.length; // 더 순회할 요소가 있는지 확인
        }
    
        @Override
        public Object next() {
            return array[nextIndex++]; // 다음 요소 반환 후 인덱스 증가
        }
    }
    
    public class Client {
        public static void main(String[] args) {
            ArrayCollection array = new ArrayCollection(5); // 크기가 5인 컬렉션 생성
            array.append(1);
            array.append(2);
            array.append(3);
            array.append(4);
            array.append(5);
    
            Iterator it = array.iterator(); // Iterator 생성
    
            while (it.hasNext()) { // 순회
                System.out.printf("%s -> ", it.next()); // 요소 출력
            }
        }
    }
  • 장점
    • 일관된 이터레이터 인터페이스를 사용해 여러 종류의 컬렉션에 대해 동일한 순회 인터페이스 제공
    • 클라이언트는 컬렉션 내부 구조 및 순회 방식을 알지 않아도 됨
    • 컬렉션의 구현과 접근하는 부분을 반복자로 분리해 결합도를 낮춤
  • 단점
    • 만일 앱이 간단한 컬렉션에서만 작동하는 경우 패턴 적용시 코드가 복잡해짐
    • 이터레이터 객체를 만드는 것이 필요한 상황인지 판단할 필요가 있음
  • JAVA 에서 반복자 패턴이 적용된 사례
    • java.util.Iterator
      • hasNext() : 순회할 요소가 있는지 확인
      • next() : 현재 커서의 요소를 출력하고 다음 위치로 커서를 이동
      • remove() : iterator 에서 next() 로 받았던 해당 요소를 삭제
        • 지원되는 경우만 가능 (보통 동시 다발적으로 같은 명령을 수행해도 안전한 컬렉션에서 제공)
          import java.util.*;
          
          public class Main {
              public static void main(String[] args) {
                  // TreeSet으로 정렬된 컬렉션 생성
                  Set<Integer> aggregate = new TreeSet<>(List.of(1, 2, 3, 4, 5));
          
                  // Iterator 생성
                  Iterator<Integer> itr = aggregate.iterator();
          
                  // 컬렉션 순회
                  while (itr.hasNext()) {
                      System.out.printf("%d 삭제 ", itr.next());
                      itr.remove(); // 요소 삭제
                  }
          
                  // 컬렉션 상태 출력
                  System.out.println(aggregate); // 출력: []
              }
          }


      • forEachRemaining() : 함수형 인터페이스를 통해 순회 코드를 단순하게 만듦
        import java.util.*;
        
        public class Main {
            public static void main(String[] args) {
                // TreeSet으로 정렬된 컬렉션 생성
                Set<Integer> aggregate = new TreeSet<>(List.of(1, 2, 3, 4, 5));
        
                // forEachRemaining() 사용
                aggregate.iterator().forEachRemaining(System.out::println);
            }
        }
        
        public class Main {
            public static void main(String[] args) {
                // HashSet 생성
                Set<String> set = new HashSet<>();
                set.add("apple");
                set.add("banana");
                set.add("mango");
        
                // forEachRemaining()로 사용자 정의 출력
                set.iterator().forEachRemaining((fruit) -> {
                    System.out.printf("My favorite food is %s!\n", fruit);
                });
            }
        }

 

 

 

 

 

 

 

 

 

 

 

 



반응형