BackEnd/Design Patterens

[Design Patterns] Template Method Pattern : 템플릿 메소드 패턴

샤아이인 2022. 1. 13.

Head First Design Patterns 책을 읽으며 정리한 내용입니다. 문제가 될 시 글을 내리도록 하겠습니다!

 

Template Method Pattern 이란?

 

Template Method Pattern - 메소드에서 알고리즘의 골격을 정의합니다.
알고리즘의 여러 단계 중 일부는 서브클래스에서 구현할 수 있습니다. 템플릿 메소드를 이용하면 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의 할 수 있습니다.

 

이 패턴은 일련의 단계들(메소드들의 순서) 로 알고리즘을 정의한 메소드 입니다. 메소드들을 어떤 순서로 호출하는지가 알고리즘이라 할 수 있겠습니다!

 

여러 알고리즘의 단계(메소드) 가운데 하나 이상이 추상메소드로 정의 되며, 그 추상 메소드는 서브 클래스에서 구현됩니다.

이렇게 서브클래스에서 일부분을 구현하도록 하면 알고리즘의 구조는 바꾸지 않아도 되도록 할 수 있습니다. 틀은 바뀌지 않는 것 이죠!

 

다이어그램을 확인 해 봅시다!

 

▶ AbstractClass 에는 템플릿 메소드가 들어있습니다.

▶ 알고리즘을 구현할 때 사용되는 메소드(primitiveOp1, primitiveOp2)는 구체적인 구현으로부터 분리되어 있습니다.

▶ ConcreteClass 는 여러개가 있을 수 있습니다. 각 클래스에서는 템플릿 메소드에서 알고리즘을 구현시 필요한 메소드들을 모두제공해야 합니다. 즉, abstract로 되있던 단계들을 구현하는 것 입니다.

 

아직 잘 이해안가시죠? 일단 아래 예시글 한번 쭉 읽고 다시 올라와서 읽어보시길 권장합니다! 진심입니다!

 

이 말이 무슨 의미인지 다음 단락부터 예시를 통하여 설명해 보겠습니다!


패턴 소개

커피와 차는 개발자들에게 필수죠! 아니다 정확히 말하면 카페인이 필수인거 군요?

커피와 차를 만들기 위한 클래스를 한번 만들어 봅시다!

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();
        pourInCup();
        addLemon();
    }

    public void boilWater() {System.out.println("물 끓이기");}
    public void steepTeaBag() {System.out.println("차를 우리는 중");}
    public void addLemon() {System.out.println("레몬 추가하는 중");}
    public void pourInCup() {System.out.println("컵에 따르는 중");}
}
 

흠 자세히 보니... 아니다 대충 봐도 코드가 너무 중복되고 있지 않나요? 이렇게 중복되게 코드를 구성할거면 이 글을 읽으러 오시지 않으셨겠죠?

 

공통되는 부분을 뽑아내는 추상화 작업을 진행해 봅시다! 대략 다음과 같을 것 입니다.

출처 -  https://swk3169.tistory.com/277
 

위의 다이어그램에서도 보이듯 prepareRecipe()는 서브클래스마다 조금씩 다르기 때문에 추상메소드로 선언하여 서브클래스에서 구현하도록 만들었습니다!

 

또다른 공통점은 없을까요? 이게 끝인것 일까요? 뭐가 남은 것 일까요?

 

두가지 음료를 만드는 알고리즘이 똑같다는 것 을 알 수 있습니다!

 

대략적으로 "물을 끓이고 → 뜨거운 물을 이용하여 커피, 홍차를 우리고 → 컵에 따르고 → 첨가물을 추가한다" 라는 큰 알고리즘이 동알한 것 입니다! 그럼 알고리즘에 해당하는 것이 무엇일까요? 바로 prepareRecipe() 겠군요!!

 

prepareRecipe() 까지 추상화 시킬 수 있는 방법을 찾아봅시다!

 

prepareRecipe() 추상화 하기

1) 잘 생각해보면 Coffee를 우려내는 것과 홍차를 우려내는 것은 별로 다르지가 않습니다. 뭘 우려내는지만 다를 뿐이죠!

따라서 brew()라는 이름으로 메소드를 만들고, 커피 나 홍차 에서 모두 brew()를 사용하도록 합시다!

마찬가지로 설탕과 우유를 커피에 추가하는 것 이나, 레몬을 차에 추가하는 것 이나 음료에 뭔가 추가한다는 것은 똑같으니 addCondiments() 라는 메소드를 양쪽 모두에 추가해 줍시다!

void prepareRecipre() {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
}
 

2) prepareRecipe() 를 코드에 집어넣어 봅시다! CaffeineBeverage라는 수퍼클래스부터 시작합니다.

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

    abstract void brew();
    abstract void addCondiments();

    public void boilWater() {
        System.out.println("물 끓이기");
    }
    public void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
}
 

우선 CaffeineBeverage는 추상클래스 입니다. 잘보면 내부에 추상메소드가 존재하고 있는걸 볼 수 있습니다.

Tea를 만들던 Coffee를 만들던 똑같은 prepareRecipe()를 사용합니다. 따라서 서브클레스에서 변경하지 못하도록 final로 선언하였습니다.

 

또한 brew(), addCondiments() 는 추상메소드입니다. Tea와 Coffee에서 서로 다른방식으로 처리하기 때문에 추상메소드로 선언하였습니다. 서브클래스에서 알아서 하겠죠?

 

3) Coffee 와 Tea 클래스를 만들어야 겠군요. CaffeineBeverage를 상속하여 brew(), addCondiments() 만 손보면 될 것 같군요!

public class Tea extends CaffeineBeverage{
    public void brew() {System.out.println("차를 우리는 중");}
    public void addCondiments() {System.out.println("레몬 추가하는 중");}
}

public class Coffee extends CaffeineBeverage {
    public void brew() {System.out.println("필터를 통해 커피를 우려내는 중");}
    public void addCondiments() {System.out.println("설탕과 우유를 추가하는 중");}
}
 

지금까지의 여정이 템플릿 메소드 패턴 이라 할 수 있습니다. CaffeineBeverage 클래스의 보면 그 안에 "템플릿 메소드" 가 들어있습니다!

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

prepareRecipe()가 바로 템플릿 메소드 입니다!

왜냐하면 어떤 알고리즘(행동 순서) 에 대한 템플릿(주형 틀) 역할을 하고있기 때문입니다. 템플릿 내부에서 알고리즘이란 각 메소드의 순서로 표현될 수 있습니다. 커피를 컵에 따르고 우유를 넣을지? 우유를 넣고 커피를 따를지? 그 순서가 알고리즘이라 할 수 있겠군요!

 

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


잠시 조금 다른내용을 배워 볼까요? 바로 Hook 에 대해 알아볼 것 입니다!

후크(Hook)는 추상클래에서 선언되는 메소드이긴 하지만, 기본적인 내용만 구현되어 있거나 아무 코드도 없는 메소드 입니다.

 

이렇게 하면 서브클래스에서 다양한 위치에서 알고리즘에 끼어들 수 있게됩니다. 그럼 어떤 용도로 사용되는 것 일까요? 다음 코드를 보시죠!

public abstract class CaffeineBeverageWithHook {
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        if(customerWantsCondiments()) {
            addCondiments();;
        }
    }

    abstract void brew();
    abstract void addCondiments();

    public void boilWater() {System.out.println("물 끓이기");}
    public void pourInCup() {System.out.println("컵에 따르는 중");}
    boolean customerWantsCondiments() {return true;}
}
 

customerWantsCondiments() 라는 구상메소드에 의해 실행여부가 결정되는 조건문이 추가되었습니다.

손님이 첨가물을 넣아달라고 했을때만, 즉 customerWantsCondiments() 에서 true를 반환해야만 첨가물을 추가하게 됩니다.

 

customerWantsCondiments() 가 서브클레스에서 필요에 따라서 오버라이딩 할 수 있는 Hook 입니다.

지금은 기본메소드가 구현되어 있군요!

 

 

후크의 활용

후크를 사용하려면 서브클래스에서 오버라이딩 해야합니다.

위의 코드에서는 음료에서 첨가물을 추가할지 말지 여부를 결정하기 위해 후크를 사용했습니다.

 

그런데 손님이 첨가물을 추가하고싶은지 어떻게 알죠? 한쪽눈을 감고 관심법을 쓰면 될까요? 혹시 저 마구니 인가요? 그냥 손님한테 물어봅...

public class CoffeWithHook extends CaffeineBeverageWithHook {
    public void brew()  {
        System.out.println("필터를 통해 커피를 우려내는 중");
    }
    public void addCondiments() {
        System.out.println("설탕과 우유를 추가하는 중");
    }

    boolean customerWantsCondiments() {
        String answer = getUserInput();

        if(answer.toLowerCase().startsWith("y")){
            return true;
        }else{
            return false;
        }
    }

    private String getUserInput() {
        String answer = "";
        System.out.println("커피에 우유와 설탕을 넣어 드릴까요? (y/n) ");
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        try{
            answer = in.readLine();
        }catch (IOException ioe) {
            System.out.println("IO 오류");
        }

        if(answer == ""){
            return "no";
        }
        return answer;
    }
}
 

위의 코드를 보면 알 수 있듯, 후크를 오버라이딩 해서 원하는 기능을 구현하였습니다.

첨가물을 넣을지 말지 고객한태 물어본 후 고객이 입력한 내용에 따라 true or false를 반환합니다.

 

테스트 코드를 통해 확인해 봅시다!

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

        System.out.println("\n 커피 만드는중...");
        coffeeHook.prepareRecipe();
    }
}
 

위의 예를 통해 후크를 써서 알고리즘 진행을 상황에 따라 변경시키는 방법을 배울 수 있었습니다.


여기서 잠깐!!

헐리우드 원칙 (Hollywood Principle)

원칙 자체는 정말 간단합니다. 그냥 먼저 연락하지 말라는 것 입니다. 연락이 오기 전까지는 말이죠!

이 원칙을 활용하면 의존성 부패(dependency rot)을 방지할 수 있습니다.

 

어떤 고수준 구성요소가 저수준에 의존하고, 그 저수준 구성요소는 다시 고수준에 의존하고, 그 고수준 구선요소는 또 다른 저수준 구성요소에 의존하는 것 과 같은, 의존성이 복잡하게 꼬여있는 것을 "의존성 부패" 라고 부릅니다.

 

헐리우드 원칙을 사용하면 저수준 구성요소에서 시스템에 접속할 수 있지만, 언제 어떤식으로 저수준 구성요소를 사용할지는 고수준 구성요소에서 결정하게 됩니다. 즉, 고수준에서 먼저 연락을 해야만 하는 것 이죠!

 

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


이러한 헐리우드 원칙은 템플릿 메소드 패턴 에서도 확인할 수 있습니다. CaffeineBeverage 다이어그램을 다시한번 살펴봅시다.

CaffeineBeverage는 고수준 구성요소 입니다. 음료를 만드는 방법에 해당하는 알고리즘을 관리합니다. 메소드 구현이 필요할때만 서브클래스에게 요청하죠.

 

CaffeineBeverage클래스를 사용하는 Client는 구상클래스가 아닌, CaffeineBeverage에 추상화 되어있는 부분에 의존하여 사용하면 됩니다. 이렇게 하면 전체 시스템의 의존성이 줄어들겁니다.

 

Tea, Coffee 클래스에는 호출 "당하기" 전까지는 절대로 추상 클래스를 직접 호출하지 않습니다!

댓글