08. 템플릿 메서드 패턴

Mando·2023년 3월 22일
0

디자인 패턴

목록 보기
6/6

템플릿 메서드 패턴

  • 알고리즘의 골격을 정의한다.
  • 해당 패턴을 이용하면 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수 있다.

예제

public class Coffee{
    void prepareRecipe(){
    	boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }
    
    public void boidWater(){ 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();
        pourInCup();
        addLemon();
    }
    
    public void boidWater(){ System.out.println("물 끓이는 중"); }
    public void steepTeaBag(){ System.out.println("차를 우려내는 중"); }
    public void pourInCup(){ System.out.println("컵에 따르는 중"); }
    public void addLemon(){ System.out.println("레몬을 추가하는 중"); }
}
  1. Coffe, Tea 클래스는 1,3번 코드가 중복된다. 해당 코드의 중복을 제거해보자
  2. Coffe, Tea 클래스의 2,4번 코드는 뜨거운 물을 이용해서 커피 또는 찻잎을 우려내는 것, 각 음료에 맞는 첨가물을 넣는 것으로 추상화가 가능할 것 같다.
public abstract class CaffeineBeverage {

    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    protected abstract void addCondiments();

    protected abstract void brew();

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

}

서브 클래스가 알고리즘 구조를 바꿀 수 없어야 한다. 따라서 오버라이드하지 못하도록 final로 선언했다

또한, 2,4번째 단계는 추상화를 통해 brew(), addCondiments()로 변경했다.
brew(), addCondiments()는 서브 클래스에서 처리한다. 따라서 추상 메서드로 선언했다.

public class Coffee extends CaffeineBeverage {

    @Override
    protected void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

}
public class Tea extends CaffeineBeverage{
    
    @Override
    protected void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

}

커피와 홍차를 만드는 방식을 일반화해서 알고리즘 구조(전체 처리 과정을 관리)를 base class에 넣었다.

이때, base class에서 1,3 단계는 직접 처리한다.
2,4 단계는 서브 클래스에 의존한다.

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


여기서 prepareRecipe()는 템플릿 메서드 입니다.
왜냐하면 어떤 알고리즘의 템플릿(틀) 여기서는 카페인 음료를 제조하는 알고리즘의 템플릿이다.

템플릿 내의 각 알고리즘 단계는 메소드로 표현된다.
어떤 메서드는 이 클래스에서 처리되기도 하고
서브 클래스에서 처리되는 메서드도 있다.
이때, 서브 클래스에서 구현해야 하는 메서드는 abstract로 선언해야 한다.

템플릿 메서드 패턴의 장점

  • Coffee, Tea 클래스에서 각자 알고리즘을 수행x -> CaffeinBeverage 클래스에서 알고리즘을 독점한다.
  • Coffee, Tea 클래스에 중복된 코드가 존재 -> 중복된 코드는 CaffeinBeverage에 두었기 때문에 서브 클래스에서 재사용 가능
  • 알고리즘이 바뀌면(예를 들어, 중복된 코드였던 물을 끓는 게 달라진다면) 클래스를 일일이 열여서 고쳐야 한다 -> 알고리즘이 한 군데 모여 있으므로 한 부분만 고치면 된다.
  • 새로운 음료를 추가하려면 많은 일을 해야 한다 -> 다른 음료도 쉽게 추가할 수 있는 프레임워크를 제공한다(후크와 관련)
  • 알고리즘 지식과 구현 방법이 여러 클래스에 분산되어 있다 -> CaffeinBeverage 클래스에 알고리즘 지식이 집중되어 잇으며 일부 구현만 서브 클래스에 의존한다.

후크

후크는 abstract 클래스에서 구현된 메서드이며, 아무런 기능이 없는 메서드이다.
따라서 서브 클래스에서 꼭 구현하지 않아도 되는 메서드이며
구현한다면 상황에 따라 알고리즘 진행을 다르게 할 수 있다는 장점이 있다.


//서브 클래스에서 템플릿(틀)을 건드리지 못하게 final 로 선언
abstract class AbstractClass{
	final void templateMethod(){
    	//템플릿 메서드의 각  단계는 메서드로 표현된다.
    	primitiveOperation1();
        primitiveOperation2();
        concreateOperation();
        hook();
    }
    
    //abstract로 선언된 메서드는 서브 클래스에서 구현해야 한다.
    abstract void primitiveOperation1();
    
    abstract void primitiveOperation2();
    
    //final로 선언되었으므로 서브 클래스에서 오버라이드 할 수 없다.
    final void concreateOperation(){
    	//코드
    }
    
    //아무것도 하지 않는 구상 메서드
    //서브 클래스에서 오버라이드할 수 있지만, 반드시 그래야 하는 메서드는 아니다.
  	void hook(){ }
}

후크 활용하기

public abstract class CaffeineBeverage {

    public void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }

    protected abstract void addCondiments();

    protected abstract void brew();

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
    
	//추상 클래스내에서 구현되었지만, 별 내용이 없는 메서드
    //서브 클래스는 hook를 오버라이드할 수도 있고, 그냥 무시하고 넘어갈 수도 있다.
    private boolean customerWantsCondiments() {
        return true;
    }

}
public class CoffeeWithHook extends CaffeineBeverage { // 후크를 오버라이드해서 원하는 기능을 추가

    @Override
    protected void brew() {
        System.out.println("필터로 커피를 우려내는 중");
    }

    @Override
    protected void addCondiments() {
        System.out.println("설탕과 우류를 추가하는 중");
    }

    //기본적으로는 yes여서 첨가물을 넣지만
    //서브 클래스에서 오버라이드하여 손님에게 물어보는 방식으로 기능구현했다.
    @Override
    protected boolean customerWantsCondiments() {
        String answer = getUserInput();
        if (answer.toLowerCase().startsWith("y")) {
            return true;
        }
        return false;
    }

    private String getUserInput() {
        String answer = null;
        System.out.println("커피에 우유와 설탕을 넣을까요? (y/n)? ");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        try {
            answer = br.readLine();
        } catch (IOException exception) {
            System.out.println("io exception");

        }
        if (answer == null) {
            return "no";
        }
        return answer;
    }
}
public class BeverageTestDrive {

    public static void main(String[] args) {
        CoffeeWithHook coffeeWithHook = new CoffeeWithHook();
        System.out.println("커피 준비 중");
        coffeeWithHook.prepareRecipe();
    }

}

===================================================================

커피 준비 중
물 끓이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
커피에 우유와 설탕을 넣을까요? (y/n)? 
y
설탕과 우류를 추가하는 중

Q : 템플릿 메서드 패턴에서 추상 메서드/후크 를 각각 어느 때 사용해야 하나요?
A :

  • 서브 클래스가 알고리즘의 특정 단계를 필수적으로 제공해야 한다면 추상 메서드를 사용
  • 서브 클래스가 알고리즘의 특정 단계를 선택적으로 제공한다면 후크를 사용

Q : 추상 메서드가 너무 만아지면 서브 클래스에서 일일이 추상 메서드를 구현해야 하므로 좋지 않을 것 같다.
A : 따라서 알고리즘의 단계를 쪼개는 기준을 잘 생각해봐야 한다.
또한 필수가 아닌 부분을 후크로 구현하면 그 추상 클래스의 서브 클래스를 만들 때 무조건 해당 부분을 구현하는 게 아니므로 부담이 줄어든다.

할리우드 원칙

할리우드 원칙을 활용하면 의존성 부패를 방지할 수 있다.
할리우드 원칙을 사용하면, 저수준 구성 요소가 시스템에 접속할 수는 있지만 어떻게 그 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.

의존성 부패란?
의존성이 복잡하게 꼬여있는 상황으로 고수준 구성 요소가 저수준 구성 요소에 의존하고 저수준 구성 요소가 고수준 구성 요소에 의존(이하 순환 의존성)하면 시스템 디자인을 파악하기 힘들다.

할리우드 원칙과 템플릿 메서드 패턴

  • CaffeineBeverage는 고수준 구성 요소, Tea Coffee는 저수준 구성 요소이다.
  • 이때, CaffeinBeverage이 알고리즘 틀에 대한 정의를 가지고 있고 메서드 구현이 필요한 상황에서만 서브클래스를 불러낸다.
  • CaffeinBeverage 클래스의 클라이언트는 CaffeinBeverage와 같은 추상 클래스에만 의존하므로 시스템 의존성을 줄일 수 있다.

템플릿 메서드 패턴과 프레임워크

템플릿 메서드 패턴은 프레임워크를 만드는 데 휼룡한 디자인 패턴이다.

프레임워크로 작업이 처리되는 방식을 제어할 수도 있으면서(추상 클래스에서 알고리즘 틀을 정의) 프레임워크에서 처리하는 알고리즘의 각 단계를 사용자가 정의할 수 있기 때문이다.(서브 클래스에서 abstract, 후크를 정의)

0개의 댓글