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

[소프트웨어 분석 및 설계] L24. Template Method Pattern, Visitor Pattern

by whiteTommy 2024. 12. 13.
반응형

Template Method Pattern

  • 템플릿 메소드 패턴이란?
    • 알고리즘의 구조 (뼈대)를 정의하고 일부 단계를 서브 클래스에서 구체적으로 구현할 수 있게 하는 행동 패턴
      • 여러 클래스에서 공통으로 사용하는 메소드를 템플릿화하여 상위 클래스에 정의, 서브 클래스마다 세부 동작을 다르게 구현
      • 알고리즘의 구조를 유지한 채로 행동을 다르게 변경할 수 있음
  • 템플릿 메소드 패턴이 필요한 상황
    • 회사 문서들을 분석하는 앱을 만들고 있다고 가정
      • 다양한 포맷 (pdf, doc, csv) 문서에 대해 일관된 형식으로 의미 있는 데이터 추출
    • 포맷에 맞게 처리 하는 부분 외에 많은 코드 중복이 발생
  • 템플릿 메소드 패턴의 아이디어
    • 알고리즘을 일련의 단계들로 나누고 이러한 단계를 메소드로 변환
    • 단일 템플릿 메소드 내부에 단계 메소드들에 대한 일련의 호출로 구성
    • 상속을 통해 추상 단계 메소드를 오버라이드 해서 구현

      • 템플릿 메소드 : 알고리즘의 뼈대를 이루는 메소드로 단계 메소드로 구성
        • 서브 클래스에서 오버라이딩 되면 안됨 (final 처리)
      • 추상 단계 메소드 : 모든 자식 클래스에서 구현해야 하는 메소드
      • 디폴트 단계 메소드 : 부모 클래스에서 기본 구현이 있어서 공통으로 쓰일 수 있는 메소드
        • 필요한 경우 디폴트 구현을 무시하고 자식에서 오버라이드 할 수 있음
  • 템플릿 메소드 패턴의 구조
  • 템플릿 메소드 패턴의 코드 예시
    • 숫자를 읽어와 숫자들을 연산한 결과를 알려주는 기능 구현
      • 모든 숫자를 더하는 기능 (sum)
      • 모든 숫자를 곱하는 기능 (multiply)

        abstract class FileProcessor {
            private String path;
        
            public FileProcessor(String path) {
                this.path = path;
            }
        
            public final int process() { // 템플릿 메서드
                try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
                    String line = null;
                    int result = getInitial();
                    
                    while ((line = reader.readLine()) != null) {
                        result = calculate(result, Integer.parseInt(line));
                    }
                    return result;
                    
                } catch (IOException e) {
                    throw new IllegalArgumentException(path + " is not found", e);
                }
            }
        
            // 추상 메서드: 서브클래스에서 구현 필요
            protected abstract int calculate(int result, int number);
            protected abstract int getInitial();
        }
        
        public class SumFileProcessor extends FileProcessor {
            public SumFileProcessor(String path) {
                super(path);
            }
        
            @Override
            protected int calculate(int result, int number) {
                return result + number; // 숫자들의 합 계산
            }
        
            @Override
            protected int getInitial() {
                return 0; // 초기값은 0
            }
        }
        
        public class MultiplyFileProcessor extends FileProcessor {
            public MultiplyFileProcessor(String path) {
                super(path);
            }
        
            @Override
            protected int calculate(int result, int number) {
                return result * number; // 숫자들의 곱 계산
            }
        
            @Override
            protected int getInitial() {
                return 1; // 초기값은 1
            }
        }
        
        public class Client {
            public static void main(String[] args) {
                FileProcessor sumFileProcessor = new SumFileProcessor("numbers.txt");
                int sumResult = sumFileProcessor.process();
                System.out.println(sumResult); // 출력: 15
        
                FileProcessor multiplyFileProcessor = new MultiplyFileProcessor("numbers.txt");
                int multiplyResult = multiplyFileProcessor.process();
                System.out.println(multiplyResult); //출력: 120
            }
        }


  • 훅 (hook) 메소드
    • 몸체가 비어 있는 단계 메소드로 자식 클래스가 선택적으로 오버라이딩 할 수 있음 (템플릿 메소드는 훅이 오버라이딩이 되지 않아도 작동함)
    • 훅 메소드는 알고리즘의 전/후에 배치되어 자식 클래스들에게 알고리즘의 추가 확장 지점을 제공
      abstract class AbstractClass{
          public final void templateMethod(){
          	Operation1();
              hook();
              Operation2();
          }
          
          protected abstract void Operation1();
          
          protected void hook(){
          
          }
          
          protected abstract void Operation2();
      }
      
      class ConcreteClass extends AbstractClass{
          protected void Operation1(){
          
          }
          
          protected void hook(){
      
      	}
          
          protected void Operation2(){
          
          }
      }
  • 템플릿 메소드 패턴의 코드 예시 (with hook)
    abstract class CoffeeTemplate {
        public final void makeCoffee() { // 템플릿 메서드
            boilWater();
            brewCoffeeGrounds();
            pourInCup();
            if (customerWantsCondiments()) { // Hook 메서드
                addCondiments();
            }
        }
    
        protected abstract void boilWater();
        protected abstract void brewCoffeeGrounds();
        protected abstract void pourInCup();
        protected abstract void addCondiments();
    
        // Hook 메서드: 기본적으로 true 반환
        protected boolean customerWantsCondiments() {
            return true;
        }
    }
    
    public class CoffeeWithHook extends CoffeeTemplate {
        @Override
        protected void boilWater() {
            System.out.println("물을 끓이는 중...");
        }
    
        @Override
        protected void brewCoffeeGrounds() {
            System.out.println("커피를 내리는 중...");
        }
    
        @Override
        protected void pourInCup() {
            System.out.println("컵에 커피를 따르는 중...");
        }
    
        @Override
        protected void addCondiments() {
            System.out.println("설탕과 크림 추가...");
        }
    
        @Override
        protected boolean customerWantsCondiments() { // Hook 메서드 오버라이드
            return false; // 기본 동작 변경: 설탕과 크림 추가하지 않음
        }
    }
    
    public class Client {
        public static void main(String[] args) {
            CoffeeTemplate coffee = new CoffeeWithHook();
            coffee.makeCoffee();
        }
    }
  • 장점
    • 클라이언트가 대규모 알고리즘의 특정 부분만 재정의 하도록 하여 알고리즘의 다른 부분에 발생하는 변경에 영향을 덜 받도록 함
    • 상위 클래스로 로직을 공통화하여 코드의 중복을 줄일 수 있고, 핵심 로직을 상위 클래스에 관리 하므로 관리가 용이해짐
  • 단점
    • 제공되는 뼈대로 인해 알고리즘 로직의 유연성이 제한 될 수 있음
    • 하위 클래스에서 구현할 때, 해당 단계 메소드가 어느 타이밍에 호출되는지 템플릿 메소드 로직을 이해할 필요가 있음
    • 로직에 변화가 생겨 상위 클래스가 수정되면 모든 서브 클래스 수정이 생김

 

 

Visitor Pattern

  • 방문자 패턴이란?
    • 알고리즘을 객체 구조에서 분리시키려는 행위 패턴
      • 노드한테 책임을 주지 말고 분리하자 
      • 각 클래스들의 데이터 구조에서 처리 기능을 분리 하여 별도의 클래스로 구현하는 패턴
      • 분리된 처리 기능은 방문자 (visitor) 를 통해 각 클래스들을 방문하면서 수행
  • 방문자 패턴이 필요한 상황
    • 그래프로 구성된 지리 정보를 사용해 작동하는 앱을 구현 중이라고 가정
      • 각 정점은 산업, 관광 지역 등 세부적인 정보를 가지는 여러 클래스로 표현
      • 정점들은 도로로 연결됨
    • 그래프를 XML 형식으로 내보내는 작업을 구현하려고 할 때, 기존 노드 클래스를 변경할 수 없는 상황이라고 가정
      • 각 노드 클래스에 export 메소드를 추가해서 그래프를 순회하며 export 를 수행하면 간단하게 처리될 수 있음
      • XML export 메소드를 모든 노드 클래스에 추가해야 하는데, 이러한 변경과 함께 버그가 발생하면 전체 앱이 망가질 수 있음
      • 노드 클래스의 주 작업은 지리 데이터를 처리 하는 것이기에 export 를 추가하는게 적절하지 않을 수 있음
      • 만약 다른 형식으로 확장을 해야 한다면 클래스 전반적으로 다시 수정해야 함
  • 방문자 패턴의 아이디어
    • 데이터를 처리하는 기능을 기존 클래스에 두는 것이 아닌, 방문자라는 별도의 클래스에 배치
      • 방문자에 의해 방문이 되면서 처리 기능 수행 (행동을 수행해야 했던 원래 객체가 방문자의 인수로 전달되면서 원래 객체의 정보에 접근할 수 있음)
    • XML export 예시에서 다음과 같이 노드 클래스 종류에 맞게 처리 기능을 구현하고 방문하면서 처리
      class ExportVisitor implements Visitor{
          public void doForCity(City c){
          ...
          }
          
          public void doForIndustry(Industry f){
          ...
          }
          
          public void doForSightSeeing(SightSeeing ss){
          ...
          }
      }
      
      for(Node node : nodes)
          node.accept(exportVisitor)
      
      public class City extends Node{
          public void accept(Visitor v){
          	v.doForCity(this);
          }
      }
      
      public class Industry extends Node{
          public void accept(Visitor v){
          	v.doForIndustry(this);
          }
      }
  • 방문자 패턴의 구조
  • 방문자 패턴의 예시 코드
    • 도형 (shape) 클래스가 있고, 방문자는 도형의 넓이를 계산하는 예시
      • Circle, Rectangle, and Triangle
        interface ShapeVisitor {
            double visit(Circle circle);    // 원 처리 메서드
            double visit(Rectangle rectangle); // 사각형 처리 메서드
            double visit(Triangle triangle);   // 삼각형 처리 메서드
        }
        
        interface Shape {
            double accept(ShapeVisitor visitor); // Visitor를 받아들임
        }
        
        class Circle implements Shape {
            private double radius;
        
            public Circle(double radius) {
                this.radius = radius;
            }
        
            public double getRadius() {
                return radius;
            }
        
            @Override
            public double accept(ShapeVisitor visitor) {
                return visitor.visit(this); // Visitor의 visit 메서드 호출
            }
        }
        
        class Triangle implements Shape {
            private double sideA, sideB, sideC;
        
            public Triangle(double sideA, double sideB, double sideC) {
                this.sideA = sideA;
                this.sideB = sideB;
                this.sideC = sideC;
            }
        
            public double getSideA() {
                return sideA;
            }
        
            public double getSideB() {
                return sideB;
            }
        
            public double getSideC() {
                return sideC;
            }
        
            @Override
            public double accept(ShapeVisitor visitor) {
                return visitor.visit(this); // Visitor의 visit 메서드 호출
            }
        }
        
        class Rectangle implements Shape {
            private double width, height;
        
            public Rectangle(double width, double height) {
                this.width = width;
                this.height = height;
            }
        
            public double getWidth() {
                return width;
            }
        
            public double getHeight() {
                return height;
            }
        
            @Override
            public double accept(ShapeVisitor visitor) {
                return visitor.visit(this); // Visitor의 visit 메서드 호출
            }
        }
        
        class AreaCalculator implements ShapeVisitor {
            @Override
            public double visit(Circle circle) {
                return Math.PI * Math.pow(circle.getRadius(), 2); // 원의 넓이 계산
            }
        
            @Override
            public double visit(Rectangle rectangle) {
                return rectangle.getWidth() * rectangle.getHeight(); // 사각형의 넓이 계산
            }
        
            @Override
            public double visit(Triangle triangle) {
                double s = (triangle.getSideA() + triangle.getSideB() + triangle.getSideC()) / 2;
                return Math.sqrt(s * (s - triangle.getSideA()) * (s - triangle.getSideB()) * (s - triangle.getSideC())); // 삼각형의 넓이 계산 (헤론의 공식)
            }
        }
        
        public class Client {
            public static void main(String[] args) {
                List<Shape> shapes = new ArrayList<>();
                shapes.add(new Circle(10));
                shapes.add(new Triangle(2,4,5));
                shapes.add(new Rectangle(3,5));
                
                double totalArea = 0.0;
                ShapeVisitor visitor = new AreaCalculator(); // AreaCalculator 생성
                
        
                for (Shape shape : shapes) {
                    totalArea += shape.accept(visitor); // 각 도형에 방문자 적용
                }
        
                System.out.println("Total Area: " + totalArea);
            }
        }
  • 장점
    • OCP 준수
      • 다른 클래스를 변경하지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 행동을 도입할 수 있음
    • SRP 준수
      • 동작을 클래스로 캡슐화 하므로 자신의 주된 책임에 집중
    • Visitor 클래스는 관련된 동작을 캡슐화 하므로 동일한 동작을 다양한 element 클래스에 재사용할 수 있음
  • 단점
    • Element 인터페이스를 구현하는 새로운 클래스가 추가될 때 visitor 에 대한 수정이 발생할 수 있음 (유지보수의 어려움)
      • 예) square 구현
        double visit (square ... ) 추가해줘야 함
        visitor 에 대한 수정이 발생
    • 런타임에 동작을 결정하므로 오버헤드가 발생
      • 예) getArea()
        10000번 -> 30000번 호출되어야 함

 

 

 

 

 

 

 

반응형