읽은 책 정리/Head Firstr Design Pattern

CHAPTER 08.템플릿 메소드 패턴

템플릿 메소드 패턴 이해를 위한 간단한 문제 제안

  • 우리는 개발을 하다보면 비슷한 메소드에 비슷한 로직을 작성할 때가 있습니다.
  • 특히 아래와 같은 상황은 개발자들에게 최적화에 대한 욕망과 갈증을 뿜뿜하게 됩니다.

스타버즈 커피 만드는 법

  1. 물을 끓인다.
  1. 끓는 물에 커피를 우려낸다.
  1. 커피를 컵에 따른다.
  1. 설탕과 우유를 추가한다.

스타버즈 홍차 만드는 법

  1. 물을 끓인다.
  1. 끓는 물에 찻잎를 우려낸다.
  1. 홍차를 컵에 따른다.
  1. 레몬을 추가한다.

  • 두 개의 레시피를 살펴보면 재료만 다르지 행동은 비슷한다는걸 알 수 있습니다.
  • 따라서 위의 레시피를 문맥 그대로 코드로 녹여낼 경우 반복적인 코드가 탄생할 것입니다.
  • 템플릿 메소드 패턴은 예시처럼 비슷한 로직을 템플릿화 하여 코드의 퀄리티를 높여주는 디자인 패턴입니다.

템플릿 메소드 패턴을 적용해보기

  • 위 사진은 레시피 절차를 코드로 구현한것입니다.
  • 코드를 보면 2번, 4번 줄을 제외하고는 절차와 로직이 완벽히 동일하다는것을 알 수 있습니다.
  • 템플릿 메소드 패턴은 이와 같은 유사하지만 조금씩 다른 로직을 abstract로 설정하여 개발자가 변화되는 부분만 재정의 하도록 유도하는 패턴입니다.
public abstract class CaffeineBeverage {
  
	final void prepareRecipe() { // 서브 클랠스가 메소드를 함부로 오버라이드 못하도록 final 선언
		boilWater();
		brew(); // 커피와 차 둘 다 사용할 수 있게 공통된 이름으로 변경
		pourInCup();
		addCondiments(); // 커피와 차 둘 다 사용할 수 있게 공통된 이름으로 변경
	}
 
	abstract void brew(); // 해당 템플릿 메소드 패턴을 사용하는 서브클래스는 brew를 정의해야함
  
	abstract void addCondiments(); // 해당 템플릿 메소드 패턴을 사용하는 서브클래스는 addCondiments를 정의해야함
 
	void boilWater() {
		System.out.println("Boiling water");
	}
  
	void pourInCup() {
		System.out.println("Pouring into cup");
	}
}
  • 여기서 부터는 템플릿 메소드 패턴의 템플릿을 직접 구현해보겠습니다.
  • 먼저 위의 prepareRecipe 로직에서 2번,4번 줄의 이름을 공통적인 느낌을 주기 위해 변경하였습니다.
  • 이외의 개발자가 해당 템플릿 메소드 패턴을 사용했을때 재정의 해야하는 영역은 absstract로 설정해줍니다.
public class Coffee extends CaffeineBeverage {
	public void brew() {
		System.out.println("Dripping Coffee through filter");
	}
	public void addCondiments() {
		System.out.println("Adding Sugar and Milk");
	}
}
public class Tea extends CaffeineBeverage {
	public void brew() {
		System.out.println("Steeping the tea");
	}
	public void addCondiments() {
		System.out.println("Adding Lemon");
	}
}
  • 서브클래스에서는 템플릿인 CaffeinBeverage를 재정의해서 사용하면 됩니다.
public class BeverageTestDrive {
	public static void main(String[] args) {
 
		Tea tea = new Tea();
		Coffee coffee = new Coffee();
 
		System.out.println("\nMaking tea...");
		tea.prepareRecipe();
 
		System.out.println("\nMaking coffee...");
		coffee.prepareRecipe();
	}
}
  • 마지막으로 테스트를 진행하도록 해보겠습니다.

수행결과

  • 지금까지 정리하자면 템플릿 메소드 패턴을 사용할 때 핵심은 변화하는 부분을 abstract으로 설정하여 개발자가 변화하는 부분만 재정의하여 사용하도록 유도하는 디자인 패턴입니다.
  • 이를 통해 템플릿 메소드는 알고리즘의 각 단계를 정의할 수 있고 서브클래스를 구현하는 개발자가 일부 단계를 구현할 수 있도록 유도할 수 있습니다.

템플릿 메소드 패턴의 장점

템플릿 메소드 패턴을 사용하지 않고 Coffe와 Tea를 둘 다 구현한 경우템플릿 메소드로 만든 CaffeinBeverage를 사용한 경우
관리 측면Coffe와 Tea 클래스 각각 관리해야 함CaffeinBeverage 클래스에서 작업을 처리함
중복 코드Coffe와 Tea 클래스에 중복 코드가 존재 합니다.CaffeinBeverage 덕분에 서브클래스에서 코드를 재사용할 수 있습니다.
수정알고리즘 변경시 변경된 서브클래스를 전부 고쳐야함알고리즘이 한 곳에 모여 있으므로 한 부분만 고치면 된다.
확장성낮음높음

템플릿 매소드 페턴의 정의

  • 템플릿 메소드 패턴은 알고리즘의 골격을 정의합니다.
    • 간단히 말하면 템플릿 메소드 패턴은 알고리즘의 템플릿을 만드는것을 의미합니다.
  • 템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있고, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브 클래스에서 재정의할 수 있습니다.

템플릿 메소드의 후크 알아보기

  • 후크(hook)는 추상 클래스에서 선언되지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어 있지 않은 메소드입니다.
  • 후크는 개발자가 원하는 다양한 용도로 사용할 수 있는 메소드입니다.
    • 이를 통해 후크는 알고리즘 진행 순서를 변경할 수 있습니다.
public abstract class CaffeineBeverageWithHook {
 
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
//고객물이 첨가물 동의를 한 경우에만 첨가물을 넣게 코드를 변경. 만약 기본값을 false로 바꾸고 싶다면 hook를 재정의하면 된다.
		if (customerWantsCondiments()) {
			addCondiments();
		}
	}
 
	abstract void brew();
 
	abstract void addCondiments();
 
	void boilWater() {
		System.out.println("Boiling water");
	}
 
	void pourInCup() {
		System.out.println("Pouring into cup");
	}
 
//customerWantsCondiments는 Hook 메소드이다.
	boolean customerWantsCondiments() {
		return true;
	}
}
  • 위처럼 후크 메소드는 오버라이드 하지 않을 경우 템플릿의 정의를 그대로 따라가게 되고 서브클래스에서 오버라이드 한 경우에는 새로운 로직을 수행하게 된다.
public class CoffeeWithHook extends CaffeineBeverageWithHook {
 
	public void brew() {
		System.out.println("Dripping Coffee through filter");
	}
 
	public void addCondiments() {
		System.out.println("Adding Sugar and Milk");
	}
 
	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("Would you like milk and sugar with your coffee (y/n)? ");

		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		try {
			answer = in.readLine();
		} catch (IOException ioe) {
			System.err.println("IO error trying to read your answer");
		}
		if (answer == null) {
			return "no";
		}
		return answer;
	}
}
  • 서브클래스에서 직접 고객이 첨가물을 받을지 안 받을지를 구현하는 예시를 진행하도록 해보겠습니다.
  • 먼저 customerWantsCondiments Hook를 재정의하여 getUserInput을 통해 고객이 첨가물을 넣을지 말지를 결정하도록 했습니다

실행결과

할리우드 원칙

  • 템플릿 메소드 패턴을 학습하면서 배울수 있는 객체지향 원칙은 할리우드 원칙 입니다.
  • 할리우드 원칙의 정의는 의존성 부패를 방지할 수 있는 원칙입니다.
  • 의존성 부패란 고수준 구성 요소가 저수준 구성 요소를 의존하고 해당 저수준 구성 요소가 또 다시 고수준 구성 요소를 의존하게 되는 반복적인 행동들로 인해 의존성이 복잡하게 꼬여있는 상황을 의존성 부패라고 정의합니다.
  • 템플릿 메소드 패턴은 할리우드 원칙을 잘 지키고 있는 패턴입니다. 예시로 진행했던 CaffeineBeverage와 서브클래스들을 보면 CaffeinBeverage는 고수준 구성요소라 음료를 만드는 알고리즘만 제공하고 있는 변경하는 부분은 서브클래스가 재정의하여 진행하는것을 알 수 있습니다.

템플릿 메소드 패턴과 전략 패턴 비교

템플릿 메소드 패턴전략 패턴
정의알고리즘 개요를 정의알고리즘을 정의
알고리즘 수행 대상서브 클래스전략패턴이 알고리즘을 조건별로 변경하여 사용
사용 방식상속클라이언트게 객체 구성으로 알고리즘 구현 여부 선택권을 부여함
적절한 사용 시기알고리즘이 유사하나 알고리즘 일부가 다른 경우템플릿 메소드 패턴보다 좀 더 유연하다.
의존성전략패턴은 구성에 의존하지만 템플릿 메소드 패턴은 상속에 의존하기에 템플릿 메소드 패턴의 의존성이 더 높음템플릿 메소드 패턴보다 낮음

정리

  • 템플릿 메소드 패턴은 알고리즘 골격을 정의한다.
  • 템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있다.
  • 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수 도 있다.
  • 템플릿 메소드 패턴은 코드 재사용에 큰 도움이된다.
  • 템플릿 메소드가 들어있는 추상 클래스는 구상 메소드, 추상 메소드, 후크를 정의할 수 있다.
  • 추상 메소드는 서브클래스에서 구현한다.
  • 후크는 추상 클래스에 들어있는 메소드로 아무 일도 하지 않거나 기본 행동만을 정의한다.
    • 서브 클래스에서 후크를 오버라이드하여 알고리즘의 흐름을 변경할 수도 있다.
  • 할리우드 원칙에 따르면 저수준 모듈은 고수준 모듈에 의해 호출 시기가 결정된다.