템플릿 메소드 패턴(Template Method Pattern)

Head First - Design Patterns
(에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠 저 | 서환수 역)
을 읽고 정리한 내용입니다.


템플릿 메소드 패턴(Template Method Pattern)

매우 많이 쓰이는 패턴이며 특히 프레임워크를 만들 때 좋은 디자인 도구인
템플릿 메소드 패턴에 대해 알아보자.

여기에 커피와 차를 만들기 위한 클래스가 있다.
우선 커피 클래스를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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("설탕과 우유를 추가하는 중");
}
}

다음은 홍차를 만들기 위한 Tea 클래스를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Coffee {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
// Coffee 클래스와 있는 메서드와 완전히 똑같다.
public void boilWater() {
System.out.println("물 끓이는 중");
}
// 차 전용 메서드
public void steepTeaBag() {
System.out.println("차를 우려내는 중");
}
// Coffee 클래스와 있는 메서드와 완전히 똑같다.
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
// 차 전용 메서드
public void addLemon() {
System.out.println("레몬을 추가하는 중");
}
}

Tea 클래스에서 Coffee 클래스에 있는 boilWater과 pourInCup 메서드가 중복되고 있다.
이 두 클래스가 거의 똑같으므로 공통적인 부분을 추상화시켜서
베이스 클래스를 만드는 게 좋지 않을까?
그렇다면 어떤 식으로 베이스 클래스를 만드는 게 좋을까?

추상화 방법

보통 이와 같이 추상화하는 경우가 많다.

이렇게 코드 재사용을 목적으로 추상화를 했다.

  • boilWater()pourInCup() 메서드가 두 클래스에서 중복되므로 수퍼클래스에서 정의한다.
  • prepareRecipe() 메서드
    • 클래스마다 다르므로 추상 메서드로 선언한다.
    • 각 서브클래스에서 prepareRecipe() 메서드를 오버라이드해서 구현한다.
  • Coffee와 Tea에만 있는 메서드들은 서브클래스에 그대로 남겨둔다.

문제점

brewCoffeeGrinds(), steepTeaBag() 등 메서드명이 너무 구체적으로 명시되어 있다.
이렇게 메서드명이 구체적일 경우 추상화가 어려워진다.


상속을 이용하는 두 가지 목적

cf) 출처: 조영호, 오브젝트(위키북스), 13장 서브클래싱과 서브타이핑 참고

  • 서브클래싱서브타이핑을 나누는 기준은 상속을 사용하는 목적이다.

서브클래싱(subclassing)

  • 자식 클래스가 부모 클래스의 코드를 재사용할 목적으로 상속을 사용한 경우

  • 자식 클래스와 부모 클래스의 행동이 호환되지 않는다.
    -> 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.

  • 클래스의 내부 구현 자체를 상속받는 것에 초점을 맞춘다.

  • 구현 상속(implementation inheritance) or 클래스 상속(class inheritance)이라고도 부른다.

서브타이핑(subtyping)

  • 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용할 목적으로
    상속을 사용한 경우이다.

  • 타입 계층을 구성하기 위해 상속을 사용하는 경우

  • 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환된다.
    -> 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.

  • 슈퍼타입 인스턴스를 요구하는 모든 곳에서 서브타입의 인스턴스를 대신 사용하기 위해
    만족해야 하는 최소한의 조건은 서브타입의 퍼블릭 인터페이스가 슈퍼타입에서 정의한 퍼블릭 인터페이스와
    동일하거나 더 많은 오퍼레이션을 포함해야 한다는 것이다.
  • -> 서브타입이 슈퍼타입의 퍼블릭 인터페이스를 상속받는 것처럼 보인다.

  • 인터페이스 상속(interface inheritance)이라고도 부른다.

    서브타이핑이 서브클래싱보다 좋다.


좀 더 추상화된 관점에서 접근해 보자.
Coffe와 Tea 사이에 또 다른 공통점은 없을까?
뜨거운 물을 이용해서 커피 또는 홍차를 우려낸다.
-> 두 가지 만드는 법의 알고리즘이 똑같다는 것을 알 수 있다.
prepareRecipe()까지 추상화할 수 있는 방법을 찾아보자.

prepareRecipe() 추상화하기

1. 첫 번째 문제점

Coffee에서는 brewCoffeeGrinds()addSugarAndMilk()를 사용하고,
Tea에서는 steepTeaBag()addLemon()를 사용한다.

커피를 우려내는 것과 티백을 우리는 것은 비슷하므로
brew()라는 이름을 가진 메서드를 만들고 두 클래스에서 사용하도록 하자.

설탕과 우유를 추가하는 것과 레몬을 추가하는 것은 음료에 뭔가를 추가하는 것이므로
addCondiments()라는 이름의 메서드를 만들어서 양쪽에서 사용하자.

  • 직접적으로 무엇을 한다는 걸 메서드 네이밍에 구체적으로 넣으면 추상화할 수 없다.
  • 메서드 이름만 바꿔도 추상화를 할 수 있다.

2. prepareRecipe() 메서드를 코드에 집어넣자.

조금 다른 방식으로 구현해야 하는 부분은 있지만,
Coffee 클래스와 Tea 클래스를 일반화하여 CaffeineBeverage클래스로 만들자.
달라져야 하는 부분만 추상 메서드로 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();

abstract void addCondiments();

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

brew()addCondiments() 이 두 메서드는 각 클래스에서
서로 다른 방식으로 처리하기 때문에 추상 메서드로 선언한다.
서브클래스에서 알아서 하도록 하자.

3. Coffee와 Tea 클래스에서 brew()addCondiments()를 처리한다.

두 클래스에서 음료를 만드는 방법은 CaffeineBeverage에서 결정된다.
우려내는 부분인 brew()와 첨가물을 추가하는 부분인 addCondiments()
베이스 클래스인 CaffeineBeverage에서 추상 메서드로 선언되었으므로
이를 오버라이딩해서 구현한다.

1
2
3
4
5
6
7
8
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("차를 우려내는 중");
}
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
1
2
3
4
5
6
7
8
public class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("필터를 통해서 커피를 우려내는 중");
}
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
}

템플릿 메소드 패턴(Template Method Pattern)

처음 만들었던 Tea와 Coffee 클래스에서는 중복된 코드가 있으며,
새로운 확장을 하려면, 또 중복 코드를 추가해야하는 상황이었다.
(알고리즘이 바뀔 경우 서브클래스를 일일이 열어서 여러 군데를 고쳐야하는 문제점)

템플릿 메소드 패턴은 견본 자체가 나와 있고,
알고리즘 중에서 변경되어야 하는 부분만 변경하는 경우에 사용하는 패턴이다.

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

  • 구현 방법: 여러 단계 중에서 하나 이상이 추상 메서드로 정의되며,
    그 추상 메서드는 서브 클래스에서 구현한다.
  • 장점: 서브 클래스에서 일부분을 구현할 수 있도록 하면서도
    알고리즘의 구조는 바꾸지 않아도 될 수 있도록 한다.
  • 추상 클래스 하나, 구현 클래스는 여러 개 있을 수 있다.

Abstract Class가 어떻게 정의되는지 더 자세히 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class AbstractClass {
final void templateMethod() {
// 서브클래스에서 알고리즘을 바꾸지 못하게 하기 위해
// 템플릿 메서드를 final로 선언한다.
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
// 서브 클래스에서 구현할 추상 메서드
abstract void primitiveOperation1();
// 서브 클래스에서 구현할 추상 메서드
abstract void primitiveOperation2();

final void concreteOperation() {
// concreteOperation 구현 코드
}
// hook은 구상 메서드인데 아무 기능도 없다.
void hook() {}
}
}

concreteOperation()는 이미 구현된 코드이므로
서브 클래스에서 오버라이딩 하지 말자.


템플릿 메서드와 후크

hook()에 대해 더 알아보자.
구상 메서드인데도 구현이 안된 빈 body로 되어 있다.
hook()이 꼭 존재해야 하는 메서드는 아니다.
필요하면 호출해서 사용하라는 optional한 느낌으로 제공하는 메서드이다.
hook()를 한 개가 아닌 여러 개를 넣을 수도 있다.

hook()빈 코드로 제공하는 이유

하위 클래스에서 자유롭게 오버라이딩하거나 하지 않거나 할 수 있게 만들기 위해서이다.
사용하고 싶을 때만 오버라이딩 한다.
만일 abstract를 붙여서 추상 메서드로 만들었다면,
하위 클래스에서 강제적으로 구현해야 하기 때문이다.

hook() 메서드 네이밍을 더 구체적으로 할 수는 없을까?

hook() 메서드는 빈 body로 만들어지므로 무슨 일을 할지 모른다.
따라서 구체적인 네이밍을 할 수 없다.


헐리우드 원칙

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

고수준인 추상 클래스에서 저수준인 하위(구현) 클래스의 메서드를 호출한다.


this VS super

  • this
    • 동적인 디스패치 (Dynamic Dispatch)
    • 런타임에 바꿀 수 있다.
  • super
  • 정적인 디스패치 (Static Dispatch)
    • super가 지칭하는 클래스는 런타임에 바꿀 수 없다.(컴파일 시점에 결정)
    • 그 자체로 강결합이다.

템플릿 메서드 패턴 VS 스트래티지 패턴

  • 템플릿 메서드 패턴
    • 상속을 사용
  • 스트래티지 패턴
    • 컴포지션을 사용
    • 객체 구성을 사용하기 때문에 더 유연하다.

정리

  • 템플릿 메서드: 알고리즘의 단계들을 정의할 때,
    서브클래스에서 구현하도록 할 수 있다.

  • 후크(hook)
    • 추상 클래스에 들어 있으며,
      아무 일도 하지 않거나 기본 행동을 정의하는 메서드
    • 서브클래스에서 오버라이드를 선택적으로 할 수 있다.

  • 서브클래스에서 템플릿 메서드에 들어있는 알고리즘을
    바꾸지 못하게 하고 싶다면 템플릿 메서드를 final로 선언한다.