반응형
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); } }
- Circle, Rectangle, and Triangle
- 도형 (shape) 클래스가 있고, 방문자는 도형의 넓이를 계산하는 예시
- 장점
- OCP 준수
- 다른 클래스를 변경하지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 행동을 도입할 수 있음
- SRP 준수
- 동작을 클래스로 캡슐화 하므로 자신의 주된 책임에 집중
- Visitor 클래스는 관련된 동작을 캡슐화 하므로 동일한 동작을 다양한 element 클래스에 재사용할 수 있음
- OCP 준수
- 단점
- Element 인터페이스를 구현하는 새로운 클래스가 추가될 때 visitor 에 대한 수정이 발생할 수 있음 (유지보수의 어려움)
- 예) square 구현
double visit (square ... ) 추가해줘야 함
visitor 에 대한 수정이 발생
- 예) square 구현
- 런타임에 동작을 결정하므로 오버헤드가 발생
- 예) getArea()
10000번 -> 30000번 호출되어야 함
- 예) getArea()
- Element 인터페이스를 구현하는 새로운 클래스가 추가될 때 visitor 에 대한 수정이 발생할 수 있음 (유지보수의 어려움)
반응형
'3학년 2학기 학사 > 소프트웨어 분석 및 설계' 카테고리의 다른 글
[소프트웨어 분석 및 설계] L23. Mediator Pattern, Memento Pattern, Observer Pattern (0) | 2024.12.13 |
---|---|
[소프트웨어 분석 및 설계] L22. State Pattern, Strategy Pattern (1) | 2024.12.13 |
[소프트웨어 분석 및 설계] L21. Interpreter Pattern, Iterator Pattern (1) | 2024.12.13 |
[소프트웨어 분석 및 설계] L19. Facade Pattern, Flyweight Pattern, Proxy Pattern (0) | 2024.12.13 |
[소프트웨어 분석 및 설계] L18. Composite Pattern, Decorator Pattern (0) | 2024.12.13 |