헤드퍼스트 디자인패턴 Chapter8 - 템플릿 메서드 패턴

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 8. 알고리즘 캡슐화하기 - 템플릿 메서드 패턴

예제 프로젝트 - 커피와 홍차

  • 커피와 홍차의 공통점은 카페인이 있다는 점과 만드는 방법이 매우 비슷한 방법으로 만들어진다는 점이다.

Coffee 클래스와 Tea 클래스 만들기

  • 다음은 커피를 만드는 클래스이다.
public class Coffee {
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }
    
    public void boilWater() {
        System.out.println("물 끓이는 중");
    }
    
    public void brewCoffeeGrinds() {
        System.out.println("필터로 커피 우려내는 중");
    }
    
    public void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
    
    public void addSugarAndMilk() {
        System.out.println("설탕과 우유를 추가하는 중");
    }
}
  • 그리고 다음은 홍차를 만드는 클래스이다.
public class Tea {
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        addLemon();
        pourInCup();
    }
    
    public void boilWater() {
        System.out.println("물 끓이는 중");
    }
    
    public void steepTeaBag() {
        System.out.println("티백을 우려내는 중");
    }
    
    public void addLemon() {
        System.out.println("레몬을 추가하는 중");
    }
    
    public void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
}

커피와 홍차 클래스 추상화하기

prepareRecipe() 메서드 추상화

  • 커피나 홍차를 우려내는 일은 거의 같다.
  • 설탕이나 레몬 등 첨가물을 넣는 일 또한 거의 같다.
void prepareRecipe() {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
}

CaffeineBeverage 추상화 슈퍼클래스

public abstract class CaffeineBeverage {
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    // Coffee와 Tea에서 서로 다르게 처리하므로 추상 메서드로 선언
    // 서브클래스가 알아서 처리하도록 둠
    abstract void brew();
    
    abstract void addCondiments();
    
    void boilWater() {
        System.out.println("물 끓이는 중");
    }
    
    void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
}

커피와 홍차 서브클래스

public class Coffee extends CaffeineBeverage {
    public void brew() {
        System.out.println("필터로 커피 우리는 중");
    }
    
    public void addCondiments() {
        System.out.println("설탕과 우유를 추가하는 중");
    }
}
public class Tea extends CaffeineBeverage {
    public void brew() {
        System.out.println("티백을 우리는 중");
    }
    
    public void addCondiments() {
        System.out.println("레몬을 추가하는 중");
    }
}

템플릿 메서드 패턴 알아보기

  • 위 CaffeineBeverage 클래스에서의 prepareRecipe() 메서드가 바로 템플릿 메서드이다.
    • 어떤 알고리즘의 템플릿(틀) 역할을 한다.
    • 템플릿 내에서 알고리즘의 각 단계는 메서드로 표현된다.
    • 어떤 메서드는 클래스 내에서 처리되기도 하고, 어떤 메서드는 서브클래스에서 처리되기도 한다.
      • 서브클래스에서 처리해야 하는 메서드는 abstract로 선언해야 한다.

템플릿 메서드 패턴의 장점

  1. 알고리즘이 한 군데에 모여 있으므로 수정이 필요할 때 한 부분만 수정하면 된다.
  2. 다른 서브클래스도 쉽게 추가할 수 있는 구조를 제공한다.

템플릿 메서드 패턴의 정의

  • 템플릿 메서드 패턴은 알고리즘의 골격을 정의한다.
  • 템플릿 메서드 패턴을 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수도 있습니다.
    • 템플릿이란, 일련의 단계로 알고리즘을 정의한 메서드이다.

템플릿 메서드 속 후크

  • 후크는 추상 클래스에서 선언되지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메서드이다.
  • 서브클래스는 다양한 위치에서 알고리즘에 끼어들 수 있다.
public abstract class CaffeineBeverageWithHook {
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }
    
    abstract void brew();
    
    abstract void addCondiments();
    
    void boilWater();
    
    void pourInCup();
    
    // 별 내용 없이 return true만 하는 메서드
    // 이 메서드는 서브클래스에서 필요할 때 오버라이드할 수 있는 메서드로, 후크이다.
    boolean customerWantsCondiments() {
        return true;
    }
}

후크 활용하기

public class CoffeeWithHook extends CaffeineBeverageWithHook {
    public void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }
    
    public void addCondiments() {
        System.out.println("우유와 설탕을 추가하는 중");
    }
    
    // 첨가물을 넣을지 말지 고객에게 물어본 후 입력한 내용에 따라 true/false 리턴
    public boolean customerWantsCondiments() {
        String answer = getUserInput();
        
        if (answer.toLowerCase().startsWith("y")) {
            return true;
        } else {
            return false;
        }
    }
    
    private String getUserInput() {
        String answer = null;

        System.out.print("커피에 우유와 설탕을 추가할까요? (y/n)");
        
        BufferedReader in = new BufferReader(new InputStreamReader(System.in));
        try {
            answer = in.readLine();
        } catch (IOException e) {
            System.err.println("IO 에러");
        }
        
        if (answer == null) {
            return "no";
        }
        return answer;
    }
}

후크 코드 테스트

public class BeverageTestDrive {
    public static void main(String[] args) {
        TeaWithHook teaHook = new TeaWithHook();
        CoffeeWithHook coffeeWithHook = new CoffeeWithHook();

        System.out.println("\n홍차 준비 중..");
        teaHook.prepareRecipe();

        System.out.println("\n커피 준비 중..");
        coffeeWithHook.prepareRecipe();
    }
}

할리우드 원칙

디자인 원칙 7

  • 먼저 연락하지 마라. 우리가 연락 드리겠다.
  • 저수준 구성 요소가 시스템에 접속할 수는 있지만 언제, 어떻게 그 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.
    • 즉 고수준 구성 요소가 저수준 구성 요소에게 먼저 연락하지 말라고 얘기하는 것과 같다.
  • 할리우드 원칙을 활용하면 의존성 부패(dependency rot)를 방지할 수 있다.
    • 의존성 부패 : 어떤 고수준 구성 요소가 저수준 구성 요소에 의존하고, 그 저수준 구성 요소는 다시 고수준 구성 요소에 의존하는 식으로 의존성이 복잡하게 꼬여있는 상황
  • 템플릿 메서드 패턴으로 디자인하면 서브클래스에게 우리가 먼저 연락할테니 연락하지 말라고 하는 것과 같다.

자바 API 속 템플릿 메서드 패턴

  • Arrays 클래스의 정렬
public static void sort(Object[] a) {
    Object aux[] = (Object[]) a.clone();
    mergeSort(aux, a, 0, a.length, 0);
}

// 템플릿 메서드
private static void mergeSort(Object src[], Object dest[], int low, int high, int off) {
    // 많은 코드

    for (int i = low; i < high; i++) {
        // 템플릿 메서드를 완성하려면 compareTo() 메서드를 구현해야 한다.
        for (int j = i; j > low && ((Comparable)dest[j - 1]).compareTo((Comparable)dest[j]) > 0; j--) {
            // Arrays 클래스에 이미 정의되어 있는 구상 메서드
            swap(dest, j, j - 1);
        }
    }

    // 많은 코드
}
  • AbstractList로 나만의 리스트 구현
    • ArrayList, LinkedList 같은 자바의 리스트 컬렉션은 리스트에서 필요한 기능을 구현해주는 AbstractList 클래스를 확장한다.
    • AbstractList에는 get()size() 추상 메서드에 의존하는 subList() 템플릿 메서드가 있다.
    • 다음은 String 객체만 담을 수 있는 리스트를 구현했다.
public class MyStringList extends AbstractList<String> {
    private String[] myList;
    MyStringList(String[] strings) {
        myList = strings;
    }
    
    public String get(int index) {
        return myList[index];
    }
    
    public int size() {
        return myList.length;
    }
    
    public String set(int index, String item) {
        String oldString = myList[index];
        myList[index] = item;
        return oldString;
    }
}