[디자인 패턴] 8. the Template Method Pattern

StandingAsh·2024년 10월 20일

참고: Head First Design Patterns

개요


바쁘고 피곤한 현대인에게 있어서 카페인은 필수 영양소 취급을 받고있다. 보통 카페인 보충을 위한 음료로(에너지 드링크를 제외하고) 두가지를 많이 선택한다.

  • 커피

그런데, 이 둘은 생각보다 공통점이 많다.

public class Coffee {

    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }
 
    public void boilWater() {}
    public void brewCoffeeGrinds() {}
    public void pourInCup() {}
    public void addSugarAndMilk() {}
}
 public class Tea {
 
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }
 
    public void boilWater() {}
    public void steepTeaBag() {}
    public void addLemon() {}
    public void pourInCup() {}
 }

코드로 나타내보니, boilWater()pourInCup()은 로직이 중복된다. 중복되는 코드가 있다는 것은 곧 디자인을 정리할 필요가 있다는 의미이므로, 공통된 코드를 추출해 추상화 해보자!

이렇게 공통된 메소드를 추출해 수퍼클래스로 묶어보았다. 근데 이걸로는 무언가 아쉽다. 더 추출할 부분이 없을까?
prepareRecipe() 메소드의 로직을 자세히 살펴보자.

  • 물을 끓인다
  • 끓인 물로 커피/차를 우린다
  • 컵에 담는다
  • 적절한 부재료(설탕, 우유, 레몬 등)을 추가한다.

그렇다면... prepareRecipe() 역시 추상화 할 수 있지 않을까?

메소드 추상화하기

커피의 brewCoffeeGrinds()와 차의 steepTeaBag(), 커피의 addSugarAndMilk()와 차의 addLemon()은 메소드 이름도 구현 내용도 다르지만 로직은 아주 유사하다. 따라서, 좀 더 종합적인 메소드명, 각각 brew()addCondiments()를 새로 만들자.

public abstract class CaffeineBeverage {
	final void prepareRecipe() {
		boilWater();
    	brew();
    	pourInCup();
    	addCondiments();
    }
    
    abstract void brew();
    abstract void addCondiments();
    
    void boilWater() { 물 끓이기 }
    void pourInCup() { 컵에 담기 }
}

자, 이제 CoffeeTea 클래스를 완성시켜보자.

public class Coffee extends CaffeineBeverage {
    public void brew() { 커피 내리기 }
    public void addCondiments() { 설탕과 우유 넣기 }
}
public class Tea extends CaffeineBeverage {
    public void brew() { 차 우리기 }
    public void addCondiments() { 레몬 넣기 }
}

따라서, 클래스 다이어그램을 위와 같이 수정할 수 있다.

템플릿 메소드 패턴


놀랍게도 우린 방금 템플릿 매소드 패턴을 구현해버렸다! 템플릿 메소드가 뭐길래?

우리 예제의 prepareRecipe()템플릿 메소드라고 부른다. CoffeeTea 같은 구현체들이 이 메소드를 가져와 자기 나름대로 필요에 맞게 구현해서 쓰기 때문이다. 파워포인트 템플릿을 이용해서 프레젠테이션을 만드는 것처럼.

템플릿 메소드를 활용한 덕분에 CoffeeTeabrew(), addCondiment()만 알아서 구현하면 된다. prepareRecipe()에는 전혀 손을 댈 필요가 없다!

정의

템플릿 메소드 패턴은 아래와 같이 정의한다.

알고리즘의 일부를 서브클래스에게 맡기는, 메소드의 뼈대를 제공하는 디자인 패턴.

이는 서브클래스들이 알고리즘의 특정 단계를 구조의 수정 없이 재정의할 수 있도록 해준다.

'Hook' 메소드?

일반적인 템플릿 메소드를 갖는 추상 클래스를 작성하면 아래와 같다

 abstract class AbstractClass {
    
    final void templateMethod() {
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook();
    }
    
    abstract void primitiveOperation1();
    abstract void primitiveOperation2();
    
    final void concreteOperation() { 기능 구현 }
    void hook() {}
 }

primitiveOperation 메소드들은 우리의 brew(), addCondiments() 처럼 서브클래스가 직접 구현해줘야 하는 메소드들이구나! 또, concreteOperation은 마치 boilWater() 처럼 완전히 겹치는 메소드겠네! 여기까지는 어렵지 않다. 그런데, 낯선 메소드 하나가 눈에 띈다

  • hook()은 뭐하는 메소드지..?

분명 추상 메소드는 아니다. 그런데, 아무것도 하지 않는다! 서브클래스가 오버라이딩해서 구현할 수 있겠다만, 그보다 이런 메소드가 왜 필요할까?
우선 우리의 예제를 바탕으로 hook의 활용법에 대해 알아보자.

public abstract class CaffeineBeverageWithHook {
	final void prepareRecipe() {
		boilWater();
    	brew();
    	pourInCup();
        if(customerWantsCondiments())
    		addCondiments();
    }
    
    abstract void brew();
    abstract void addCondiments();
    
    void boilWater() { 물 끓이기 }
    void pourInCup() { 컵에 담기 }
    boolean customerWantsCondiments() { return true; }
}

소비자들이 설탕, 우유, 레몬 등을 넣지 않기를 원할 수도 있으니, addCondiments()를 실행하기 전에 조건문을 하나 넣어주었다. 또한, 사용자의 부재료 선호 여부를 검증할 메소드도 customerWantsCondiments() 라고 하나 만들어보자. 이 녀석이 우리의 hook이다!

customerWantsCondiments()는 추상 메소드가 아니지만 boolean 타입의 디폴트 액션 return true;만을 하고있다. 이제 서브클래스를 구현해서 어떻게 hook을 활용할 수 있는지 살펴보자.

public class CoffeeWithHook extends CaffeineBeverageWithHook {
 
    public void brew() { 커피 내리기 }
    public void addCondiments() { 설탕과 우유 넣기 }
 
 	@Override
    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 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;
    }
}

Coffee 서브클래스에서 customerWantsCondiments()를 오버라이딩해서 사용자로부터 선호 여부를 입력 받도록 구현하였다. 그런데, 왜 추상 메소드나 고정 메소드 대신 hook을 사용한걸까?

만약에 Tea는 고객이 원하던 원하지 않던 무조건 레몬을 넣는데, 이 메소드를 추상 메소드로 만든다면 Tea 클래스에 아무 의미없는, return true;만 하는 customerWantsCondiments()를 만들어야 할 것이다. 따라서, hook을 통해 필요에 따라 구현하도록 선택권을 준 것이다.

정리하자면,

Hook은 추상 클래스에 선언된 메소드이나, 비어있거나 디폴트 액션만을 구현한 메소드이다.

hook의 존재(혹은 구현)은 필수적이지 않지만, 위의 예시처럼 오버라이딩을 통해 유용하게 활용할 수 있다.

할리우드 원칙(The Hollywood Principle)

Don't call us, we'll call you
우리를 호출하지 마라, 우리가 너를 호출할 것이다.

이 말은 즉 하이레벨(High-Level) 객체가 로우레벨(Low Level) 객체를 사용할 뿐, 로우레벨 객체가 직접 하이레벨 객체를 호출하지는 않아야 한다는 원칙이다.

정리


템플릿 메소드 패턴의 몇가지 활용 예를 살펴보면서 마무리해보자.

Arrays.sort()

Duck들의 배열이 있고, Arrays.sort() 함수를 이용해 이들을 정렬해보자. sort() 함수가 우리의 템플릿 메소드가 될 것이다. 우선, 정렬을 위한 기준을 sort()에게 전달하기 위해 Comparable 인터페이스의 compareTo() 메소드를 이용할 것이다.

public class Duck implements Comparable {
    String name;
    int weight;
  
    public Duck(String name, int weight) {
        this.name = name;
        this.weight = weight;
    }

    public int compareTo(Object object) {
        Duck otherDuck = (Duck)object;
        if (this.weight < otherDuck.weight)
            return -1;
        else if (this.weight == otherDuck.weight) {
            return 0;
        else
            return 1;
    }
}

이제 sort()Duckweight를 기준으로 정렬을 해 줄 것이다.

Duck [] ducks =  { new Duck("Daffy", 8), new Duck("Donald", 10), ... }
Arrays.sort(ducks);

JFrame

public class MyFrame extends JFrame {
    ...
    
    @Override
    public void paint(Graphics graphics) {
        super.paint(graphics);
        String msg =I rule!!;
        graphics.drawString(msg, 100, 100);
    }
    
	...
}

JFrame을 이용한 코드의 일부분이다. paint() 메소드는 JFramehook이다! 따라서, 디폴트로 아무 일도 하지 않지만 위 코드처럼 오버라이딩하여 구현해준다면 창에 메세지를 띄울 수 있다.

Applets

public class MyApplet extends Applet {
    String message;
 
    public void init() {
        message =Hello World, I’m alive!;
        repaint();
    }
 
    public void start() {
        message =Now I’m starting up...;
        repaint();
    }
 
    public void stop() {
        message =Oh, now I’m being stopped...;
        repaint();
    }
 
    public void destroy() {
        // applet is going away...
    }
 
    public void paint(Graphics g) {
        g.drawString(message, 5, 15);
    }
}
  • init(), start(), stop(), destroy(), paint()는 모두 hook 메소드이다!
  • repaint()는 견고한(Concrete) 메소드이다. Applet에 정의되어 있으므로, 서브클래스에서 사용할 수 있다.
profile
우당탕탕 백엔드 생존기

0개의 댓글