본문 바로가기
JAVA/DesignPattern

디자인패턴 - 템플릿 메소드 패턴 (template method pattern)

by 램쥐뱅 2016. 6. 16.

템플릿 메소드 패턴 (template method pattern)


메소드에서 알고리즘의 골격을 정의한다.

알고리즘의 여러 단계 중 일부는 서브클래스에서 구현할 수 있다.

템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의 할 수 있다.


알고리즘의 틀을 만들기 위한 패턴이다.

이 패턴에서 틀(템플릿)이란 일련의 단계들로 알고리즘을 정의한 메소드다. 여러 던계 가운데 하나 이상이 추상 메소드로 정의되며, 그 추상 메소드는 서브클래스에서 구현된다. 이렇게 하면 서브클래스에서 일부분의 단계를 구현할 수 있도록 하면서도 알고리즘의 구조는 바꾸지 않아도 되도록 할 수 있다.



템플릿 메소드 패턴 클래스 다이어그램




커피와 차가 만들어 지는법을 비교해보자.


1. 커피 만드는 법

1) 물을 끓인다.

2) 끓는 물에 커피를 우려낸다.

3) 커피를 컵에 따른다.

4) 설탕과 우유를 추가한다.


2. 홍차 만드는 법

1) 물을 끓인다.

2) 끓는 물에 차를 우려낸다.

3) 차를 컵에 따른다.

4) 레몬을 추가한다.


거의 똑같지 않는가???


클래스로 만들어보면..


public class Coffee {

void prepareRecipe() {

boilWater();

brewCoffeeGrinds();

pourInCup();

addSugarAndMilk();

}


public void boilWater() {

System.out.println("물 끓이는 중");

}


public void breqCoffeeGrinds() {

System.out.println("필터를 통해 커피를 우려내는 중");

}


public void pourInCup() {

System.out.println("컵에 따르는 중");

}


public void addSugarAndMilk() {

System.out.println("설탕과 우유를 추가하는 중");

}

}



public class Tea {

void prepareRecipe() {

boilWater();

steepTeaBag();

pourInCup();

addLemon();

}


public void boilWater() {

System.out.println("물 끓이는 중");

}


public void steepTeaBag() {

System.out.println("차를 우려내는 중");

}


public void pourInCup() {

System.out.println("컵에 따르는 중");

}


public void addLemon() {

System.out.println("레몬을 추가하는 중");

}

}



코드가 중복되어 있으면 디자인을 고쳐야 하지 않을까 생각해보는 것이 좋다.

Coffee하고 Tea 클래스가 거의 똑같으니까 공통적인 부분을 추상화 시켜서 클래스는 만드는게 좋지 않을까??



추상화 시켜보자!



위와 같은 디자인이 정말 괜찮은가?!

뭔가 다른 공통점을 간과하고 넘어가진 않았는가?!



Coffee와 Tea의 만드는 법을 다시 한번 살펴 보면

두 가지 만드는 법의 알고리즘이 똑같다는 것을 알 수 있다.



1. 물을 끓인다.

2. 뜨거운 물을 이용하여 커피 또는 홍차를 우려낸다.

3. 만들어진 음료를 컵에 따른다.

4. 각 음료에 맞는 첨가물을 추가한다.



이제 prepareRecipe() 까지 추상화를 시킬 수 있는 방법을 찾아보자.


Coffee                                        Tea


void prepareRecipe() {                    void prepareRecipe() {

boilWater();                                boilWater();

brewCoffeeGrinds();                    steepTeaBag();

pourInCup();                               pourInCup();

addSugarAndMilk();                    addLemon();

}                                                } 


첫번째 문제는 Coffee와 Tea에서 서로 다른 메소드를 사용하고 있는 것이지만

따지고 보면 커피를 우려내는 것과 홍차를 우려내는 것은 별로 다르지 않다.

이와 마찬가지로 설탕과 우유를 추가하는 것이나 레몬을 추가하는 것도 동일하다고 볼수있다.


일반화 하여 다시 정리해본다면.


void prepareRecipe() {

boilWater();

brew();

pourInCup();

addCondiments();

}


요렇게 prepareRecipe()를 준비할수 있게 되었다.




이제 템플릿 메소드 패턴을 이용해서 만들어보자


public abstract class CaffeineBeverage {


void final prepareRecipe() {

boilWater();

brew();

pourInCup();

addcondiments();

}


abstract void brew();


abstract void addcondiments();


void boilWater() {

System.out.println("물 끓이는 중");

}


void pourInCup()[ {

System.out.println("컵에 따르는 중");

}

}



public class Coffee extends CaffeineBeverage {


@Override

void brew() {

System.out.println("필터를 통해 커피를 우려내는 중");

}


@Override

public void addCondiments() {

System.out.println("설탕과 우유를 추가하는 중");

}

}



public class Tea extends CaffeineBeverage {


@Override

void brew() {

System.out.println("차를 우려내는 중");

}


@Override

public void addCondiments() {

System.out.println("레몬을 추가하는 중");

}

}




차를 만들어 보자.


1. Tea 객체를 만들고.

Tea myTea = new Tea();


2. 템플릿 메소드를 호출한다.

myTea.prepareRecipe(); 카페인 음료를 만들기 위한 알고리즘이 돌아간다.


3. 물을 끓인다.

boilWater(); 이 단계는 CaffeineBeverage 에서 처리된다.


4. 이제 차를 우려낸다. 

brew(); 이방법은 서브클래스만 알고있다.


5. 차를 컵에 따른다.

pourInCup(); 이 단계도 공통적인 부분이기 때문에 Caffeinebeverage 에서 맡아서 처리된다.


6. 마지막으로 첨가물을 추가한다.

addCondiments(); 첨가물은 음료마다 다르기때문에 서브클래스에서 처리된다.


이렇게 템플릿 메소드에서는 알고리즘의 각 단계들을 정희하며, 그중 한 개 이상의 단계가 서브클래스에 의해 제공 될수 있다.



템플릿 메소드로 부터 얻을수 있었던것.


처음의 Tea, Coffee클래스                                        템플릿 메소드 패턴 적용한 Tea, Coffee 클래스


Coffee와 Tea가 각각 작업을 처리,                              CaffeineBeverage 클래스에서 작업을 처리,

두 클래스에서 각자 알고리즘을 수행한다.                    알고리즘을 혼자 독점한다.


Coffee와 Tea에 중복된 코드가 있다.                           CaffeineBeverage 덕분에 서브클래스에서 코드를 재사용가능.


알고리즘이 바뀌면 서브클래스를 일일이                     알고리즘이 한 군데에 모여 있기 때문에 

열어서 여러군데를 고쳐야 한다.                                그 부분만 고치면 된다.


클래스 구조상 새로운 음료를 추가하려면                    다른 카페인 음료도 쉽게 추가할 수 있는 프레임워크를 제공한다.

꽤 많은 작업을 해야 한다.                                        카페인 음료를 추가할 떄 몇 가지 메소드만 추가하면 된다.


알고리즘에 대한 지식과 구현 방법이                          CaffeineBeverage 클래스에 알고리즘에 대한 지식이 집중

여러클래스에 분산되어 있다.                                    되어 있으며 일부 구현만 서브클래스에 의존한다.




템플릿 메소드와 후크


후크(hook)는 추상클래스에서 선언되는 메소드긴 하지만

기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메소드 이다.

이렇게 하면 서브클래스 입장에서는 다양한 위치에서 알고리즘에 끼어들수 있다.



어떻게 쓰일수 있을까?


public abstract class CaffeineBeverageWithHook {


void final prepareRecipe() {

boilWater();

brew();

pourInCup();

if ( customerWantsCondiments() ) {

addcondiments();

}

}


abstract void brew();


abstract void addcondiments();


void boilWater() {

System.out.println("물 끓이는 중");

}


void pourInCup()[ {

System.out.println("컵에 따르는 중");

}


boolean customerWantsCondiments() {    //이 메소드는 서브클래스에서 필요에 따라

return true;                                    //오버라이드 할수 있는 메소드이므로 후크이다.

}

}


여기서 customerWantscondiments() 메소드는 별 내용이 없는 기본 메소드를 구현해 놓은 후크 메소드이다.



후크는 서브클래스에서 적절하게 오버라이드 해서 사용하면 된다.


public class CoffeeWithHook extends CaffeineBeverageWithHook {


@Override

void brew() {

System.out.println("필터를 통해 커피를 우려내는 중");

}


@Override

public void addCondiments() {

System.out.println("설탕과 우유를 추가하는 중");

}


@Override

public boolean customerWantsCondiments() {

String answer = getUserInput();

if( answer.toLowerCase().startWith("y")) return true;

else return false;

}


private String getUserInput() {

// 입력받는 로직

}

}


이처럼 알고리즘에서 필수적이지 않은 부분을 필요에 따라 선택적으로 서브클래스에서 구현하든 말든 하도록 하는 경우에 후쿠를 사용할 수 있다.






헐리우드 원칙과 템플릿 메소드 패턴


헐리우드 원칙


먼저 연락하지 마세요. 저희가 연락 드리겠습니다.


이 디자인 원칙을 확용하면 의존성 부패(dependency rot)를 방지 할수 있다.

어떤 고수준 구성요소가 저수준 구성요소에 의존하고, 그 저수준 구성요소는 다시 고수준 구성요소에 의존하고, 그 고수준 구성요소는 다시 또 다른 구성요소에 의존하고.. 이런 식으로 의존성이 복잡하게 꼬여있는 것을 의존성 부패라고 한다.


헐리우드 원칙을 사용하면, 저수준 구성요소에서 시스템에 접속을 할수는 있지만, 언제 어떤 식으로 그 구성요소들을 사용할지는 고수준 구성요소에서 결정하게 된다.

즉 저수준 구성요소는 컴퓨테이션에 참여할 수는 있지만 절대 고수준 구송요소를 직접 호출하면 안된다는 것이다.


CaffeineBeverage 디자인을 이용해 본다면..



CaffeineBeverage는 고수준 구성요소 이다. 음료를 만드는 방법에 대당하는 알고리즘을 장악하고있고, 메소드 구현이 필요한 상태에서만 서브클래스를 불러낸다.


Coffee와 Tea 와 같은 서브클래스는 각각의 자질구레한 메소드 구현을 제공하기 위한 용도로만 쓰인다.

이와같은 서브클래스들은 고수준 구성요소인 CaffeineBeverage 클래스로부터 호출 당하기 전까지는 절대로 CaffeineBeverage 추상 클래스를 직접 호출하지 않는다.


이렇게 함으로 caggeinebeverage 클래스의 클라이언트에서는 Tea나 Coffee 같은 구상 클래스가 아닌 CaffeineBeverage에 추상화 되어 있는 부분에 의존하게 된다. 그렇게 함으로써 전체 시스템의 의존성이 줄어들 수 있다.













참고.

Head First Design Patterns.

댓글