템플릿 메소드 패턴(Template Method Pattern) ; 캡슐화

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

헐리우드 원칙

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

  • 의존성 부패 방지
    • 의존성 부패란? 고수준 -> 저수준 -> 고수준 -> 다른 저수준 의존성이 꼬여있는 상태
  • Client –> 고수준 구성요소 –> 저수준 구성요소 (단방향 의존성)
  • 따라서 템플릿 메소드 패턴에서는 가급적이면 템플릿 -> 서브클래스 의존성만 존재하도록 설계 할 것

책에서 나오는 예제 - 커피가게에서 홍차를 추가로 판매하기로 결정

커피의 제조 과정

  1. 물을 끓인다.
  2. 끓는 물에 커피를 우려낸다.
  3. 커피를 컵에 따른다.
  4. 설탕과 우유를 추가한다.

홍차 제조 과정

  1. 물을 끓인다
  2. 끓는 물에 차를 우려낸다.
  3. 차를 컵에 따른다.
  4. 레몬을 추가한다.

각 제조 과정에서 공통점이 보인다.
–> 공통점이 보이면 우선 추상화에 대해 고려해 봐야 한다.
–> 템플릿 메소드 패턴으로 추상화

CaffeineBeverage

템플리 메소드 패턴을 적용하여, 알고리즘 골격을 갖고 있는 추상 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public abstract class CaffeineBeverage {

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

protected void hook() {
// 아무것도 구현하지 않은 메소드
// Hook 메소드를 통해 서브클래스에게 조금 더 확장할 수 있는 기회를 준다.
}

private void boilWater() {
System.out.println("물을 끓인다");
}

private void pourInCup() {
System.out.println("음료를 컵에 따른다.");
}

protected abstract void brew();

protected abstract void addCondiments();
}

Tea

템플릿 메소드를 상속받는 서브클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Tea extends CaffeineBeverage {
@Override
protected void addCondiments() {
System.out.println("레몬을 추가한다.");
}

@Override
protected void brew() {
System.out.println("끓는 물에 차를 우려낸다.");
}

@Override
protected void hook() {
System.out.println("홍차를 식히는 중...");
}
}

Coffee

템플릿 메소드를 상속받는 서브클래스

1
2
3
4
5
6
7
8
9
10
11
public class Coffee extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("필터로 커피를 우려낸다.");
}

@Override
protected void addCondiments() {
System.out.println("설탕과 커피를 추가한다.");
}
}

테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
결과
물을 끓인다
끓는 물에 차를 우려낸다.
음료를 컵에 따른다.
레몬을 추가한다.
홍차를 식히는 중...

물을 끓인다
필터로 커피를 우려낸다.
음료를 컵에 따른다.
설탕과 커피를 추가한다.
*/
class CaffeineBeverageTest {

@Test
void caffeineBeverageTest() throws Exception {
// given
CaffeineBeverage myTea = new Tea();
CaffeineBeverage myCoffee = new Coffee();

// when / then
myTea.prepareRecipe();
myCoffee.prepareRecipe();

assertThat(myTea).isInstanceOf(CaffeineBeverage.class);
assertThat(myCoffee).isInstanceOf(CaffeineBeverage.class);
}
}

Spring 속 Template Method

org.springframework.web.servlet.mvc.AbstractController

handleRequest라는 템플릿 메소드를 가지고 있고, 그 안에서 추상 메소드인 handleRequestInternal을 호출하고 있다.
AbstractController 를 상속받는 컨트롤러들은 handleRequestInternal 메소드를 반드시 구현해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class AbstractController extends WebContentGenerator implements Controller {

// ...

@Override
@Nullable
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
throws Exception {

if (HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", getAllowHeader());
return null;
}

// Delegate to WebContentGenerator for checking and preparing.
checkRequest(request);
prepareResponse(response);

// Execute handleRequestInternal in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
return handleRequestInternal(request, response);
}
}
}

return handleRequestInternal(request, response);
}

/**
* Template method. Subclasses must implement this.
* The contract is the same as for {@code handleRequest}.
* @see #handleRequest
*/
@Nullable
protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception;
}

결론

템플릿 메소드 패턴은 알고리즘을 골격화하여 구조를 잡아주고, 구현이 필요한 부분에 대한 책임만 서브클래스가 감당하므로,
중복 제거 객체의 관리와 중복 제거 측면에서 많은 이점을 갖게 된다.

하지만, 상속 을 반드시 필요로 하기 때문에

  • 서브클래스 간 공통 부분이 얼마나 존재하는 지
  • 복잡도가 증가하진 않은 지
  • 서브클래스 간 의존하고 있는 부분은 없는 지

꼼꼼하게 따지고 적용해야할 패턴이다. 특히 알고리즘 골격이 굳이 필요 없다면 Strategy pattern 을 적용하여,
추상화 하는 것도 대안 방법 중에 하나이다.

생각해보자

상속은 보면 볼 수록 계륵에 해당하는 기술 인 것 같다. 비즈니스 로직을 구현하면서 상속을 써본 적이 거의 없는 것 같은데,
곰곰히 생각해보면 공통부분을 상속을 통해 제거하고, 유연하게 설계 한다라는 상속의 장점보다는

  • 서브클래스에서 슈퍼클래스의 멤버에 접근해야 하는 경우
  • 서브클래스에서 멤버에 set을 해야 하는 경우
  • 슈퍼클래스의 결함이 서브클래스에게도 전파된다는 것

이런 점에서 상속을 기피하게 되는 것 같다. 차라리 인터페이스로 추상화할 메소드만 별도로 분리하는 방식을 선호하게 된다.

상속에 대해서 한번 더 깊이 조사해봐야할 것 같다는 생각이 들었다.

스트레터지 패턴 vs 템플릿 메소드 패턴

템플릿 메소드의 키워드가 알고리즘, 캡슐화 로 압축할 수 있는데, 스트레터지 패턴도 알고리즘을 캡슐화 한다는 점에서
역시 헷갈리기 시작했다. 따라서 다시금 비교해보면

  • 스트레터지 패턴 : 바꿔 쓸 수 있는 행동을 캡슐화하고, 어떤 행동을 할 지는 구현체에게 맡긴다.
  • 템플릿 메소드 : 알고리즘의 일부 단계를 구현하는 것을 서브클래스에게 위임한다.

전부냐 vs 일부냐의 문제이다. 알고리즘 전부를 추상화한다면, 추상클래스로 둘 필요가 없게되고 대부분 스트레터지 패턴은 인터페이스로 설계한다.
반면 템플릿 메소드는 알고리즘의 일부만 서브클래스에게 책임을 이관해야 하므로, 반드시 클래스 or 추상클래스로 설계해야 한다.

헐리우드 원칙 vs 의존성 역전 원칙

헐리우드 원칙에 대해 고민하면서, 앞에서 의존성 역전 원칙에 대해서도 다루어 봤었는데
문득 어떤 차이가 있나 궁금해졌다. 역시나 목적 관점에서 차이를 살펴본다면

  • 헐리우드 원칙은 의존성의 방향에 관한 문제
  • 의존성 역전 원칙은 추상화 된 것에 의존해야 된다는 원칙

참고자료