11. 템플릿 메서드 패턴 (Template Method)
일의 순서(뼈대)는 부모가 정하고, 달라지는 단계만 자식이 채우게 하는 템플릿 메서드 패턴을 알아봅니다.
한 줄 요약
여러 클래스가 같은 순서로 일하는데 몇 단계만 다를 때, 순서는 부모가 한 번만 정하고 달라지는 단계만 자식이 채우게 하는 패턴이에요.
문제: 순서가 같은데 따로따로 적힌 코드
카페에서 음료를 만드는 프로그램을 만든다고 해 볼게요. 커피와 녹차는 만드는 순서가 거의 같아요.
| 순서 | 커피 | 녹차 |
|---|---|---|
| 1 | 물을 끓여요 | 물을 끓여요 |
| 2 | 원두에 물을 부어 내려요 | 찻잎을 우려요 |
| 3 | 컵에 따라요 | 컵에 따라요 |
| 4 | 설탕을 넣어요 | 레몬을 넣어요 |
가장 먼저 떠오르는 방법은 음료마다 클래스를 만들고 순서를 각자 적는 거예요.
[커피]를 설계하자:
<만들기>를 만들자 ():
"물을 끓여요."를 출력하자
"원두에 물을 부어 내려요."를 출력하자
"컵에 따라요."를 출력하자
"설탕을 넣어요."를 출력하자
(참고: 순서는 커피와 같고, 두 단계만 달라요)
[녹차]를 설계하자:
<만들기>를 만들자 ():
"물을 끓여요."를 출력하자
"찻잎을 우려요."를 출력하자
"컵에 따라요."를 출력하자
"레몬을 넣어요."를 출력하자
"(커피)"를 출력하자
새로운 [커피]()의 <만들기>()를 실행하자
"(녹차)"를 출력하자
새로운 [녹차]()의 <만들기>()를 실행하자
실행 결과
(커피)
물을 끓여요.
원두에 물을 부어 내려요.
컵에 따라요.
설탕을 넣어요.
(녹차)
물을 끓여요.
찻잎을 우려요.
컵에 따라요.
레몬을 넣어요.
잘 돌아가요. 그런데 음료가 늘어나면 아파져요.
- 같은 순서가 복사돼요. “물 끓이기 → 우리기 → 따르기 → 곁들이기”라는 순서가
[커피]와[녹차]에 따로 적혀 있어요. 코코아, 유자차가 생기면 또 복사해요. - 순서를 바꾸면 모든 음료를 고쳐야 해요. “따르기 전에 컵을 데워 주세요”라는 규칙이 생기면, 음료 클래스를 하나하나 열어서 같은 줄을 넣어야 해요.
- 같은 단계도 복사돼요. “물을 끓여요”와 “컵에 따라요”는 모든 음료가 똑같은데, 음료마다 적혀 있어요. 물 끓이는 방법을 바꾸려면 모든 곳을 고쳐야 해요.
- 새 음료를 만들 때 단계를 빠뜨리기 쉬워요. 순서를 처음부터 새로 적다 보면, 컵에 따르는 걸 잊어도 아무도 알려 주지 않아요.
해결책: 순서는 부모가, 다른 단계는 자식이
템플릿 메서드 패턴은 순서(뼈대)를 부모 클래스 한 곳으로 올려요.
- 부모를
밑설계하자(추상 클래스)로 만들고, 일의 순서를 적은 메서드<만들기>를 둬요. 이 메서드가 바로 템플릿 메서드예요. - 모든 음료가 똑같은 단계(
<물끓이기>,<따르기>)는 부모가 직접 만들어요. - 음료마다 다른 단계(
<우리기>,<곁들이기>)는있어야 한다로 비워 두고 자식에게 맡겨요. - 꼭 바꿀 필요는 없지만 바꾸고 싶은 자식만 바꾸는 단계도 둘 수 있어요. 이런 단계를 갈고리(hook)라고 불러요.
비유: 요리책의 기본 레시피
요리책에 “기본 국 끓이기” 레시피가 있어요. “① 육수를 낸다 → ② 주재료를 넣는다 → ③ 간을 한다 → ④ 고명을 올린다”. 미역국이든 된장국이든 이 순서는 같고, ②와 ④에 무엇을 넣는지만 달라요. 국마다 레시피 전체를 새로 쓰지 않고, “② 미역, ④ 없음”처럼 빈칸만 채우면 돼요. 이 기본 레시피가 “템플릿 메서드”예요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 부모(뼈대) | [음료] (밑설계) |
일의 순서를 정하고, 같은 단계는 직접 해요 |
| 템플릿 메서드 | [음료]의 <만들기> |
단계를 정해진 순서대로 불러요 |
| 비워 둔 단계 | <우리기>, <곁들이기> |
자식이 꼭 채워야 해요 |
| 갈고리 | <곁들일까> |
기본은 참이고, 바꾸고 싶은 자식만 바꿔요 |
| 자식 | [커피], [녹차], [아메리카노] |
다른 단계만 채워요 |
하리로 만들기
같은 카페를 템플릿 메서드 패턴으로 다시 만들어 볼게요. 곁들일 것이 없는 아메리카노도 하나 더했어요.
(참고: 1단계 — 순서(뼈대)는 부모가 한 번만 정해요)
[음료]를 밑설계하자:
<만들기>를 만들자 ():
'나'의 <물끓이기>()를 실행하자
'나'의 <우리기>()를 실행하자
'나'의 <따르기>()를 실행하자
만약 ('나'의 <곁들일까>()가 참과 같다) 라면:
'나'의 <곁들이기>()를 실행하자
(참고: 2단계 — 모두 같은 단계는 부모가 직접 해요)
<물끓이기>를 만들자 ():
"물을 끓여요."를 출력하자
<따르기>를 만들자 ():
"컵에 따라요."를 출력하자
(참고: 3단계 — 음료마다 다른 단계는 자식에게 맡겨요)
<우리기>가 있어야 한다 ()
<곁들이기>가 있어야 한다 ()
(참고: 바꾸고 싶은 자식만 바꾸는 '갈고리' 단계예요. 기본은 참)
[논리]를 돌려주는 <곁들일까>를 만들자 ():
참을 돌려주자
[음료]를 바탕으로 [커피]를 설계하자:
<우리기>를 만들자 ():
"원두에 물을 부어 내려요."를 출력하자
<곁들이기>를 만들자 ():
"설탕을 넣어요."를 출력하자
[음료]를 바탕으로 [녹차]를 설계하자:
<우리기>를 만들자 ():
"찻잎을 우려요."를 출력하자
<곁들이기>를 만들자 ():
"레몬을 넣어요."를 출력하자
(참고: 아무것도 곁들이지 않는 음료는 갈고리만 바꿔요)
[음료]를 바탕으로 [아메리카노]를 설계하자:
<우리기>를 만들자 ():
"에스프레소에 물을 타요."를 출력하자
<곁들이기>를 만들자 ():
"(곁들일 것 없음)"을 출력하자
[논리]를 돌려주는 <곁들일까>를 만들자 ():
거짓을 돌려주자
'음료들'을 [새로운 [커피](), 새로운 [녹차](), 새로운 [아메리카노]()]로 정하자
'음료들'의 '음료'마다 반복하자:
"(한 잔)"을 출력하자
'음료'의 <만들기>()를 실행하자
실행 결과
(한 잔)
물을 끓여요.
원두에 물을 부어 내려요.
컵에 따라요.
설탕을 넣어요.
(한 잔)
물을 끓여요.
찻잎을 우려요.
컵에 따라요.
레몬을 넣어요.
(한 잔)
물을 끓여요.
에스프레소에 물을 타요.
컵에 따라요.
커피와 녹차는 리팩터링 전과 같은 순서로 만들어져요. 아메리카노는 갈고리 <곁들일까>가 거짓을 돌려줘서 곁들이는 단계를 건너뛰었어요.
단계별로 다시 보기
- 1단계 — 순서는 한 곳에. “물 끓이기 → 우리기 → 따르기 → 곁들이기” 순서는
[음료]의<만들기>안에만 있어요. 자식들은<만들기>를 만들지 않고, 부모의 것을 그대로 써요. - 2단계 — 같은 단계는 부모가.
<물끓이기>와<따르기>는 부모에 한 번만 있어요. 물 끓이는 방법을 바꾸려면 여기만 고치면 돼요. - 3단계 — 다른 단계는 자식이.
<우리기>와<곁들이기>는있어야 한다로 비워 두었어요.[커피]가'나'의 <우리기>()를 부르면, 부모의<만들기>안에서 불러도 자식의<우리기>가 실행돼요. 부모는 순서만 알고, 무엇을 우릴지는 몰라요. - 갈고리 — 바꾸고 싶을 때만.
<곁들일까>는 부모가참을 돌려주게 만들어 두었어요. 커피와 녹차는 손대지 않았고, 아메리카노만거짓을 돌려주게 덮어썼어요. 갈고리 덕분에 순서를 바꾸지 않고도 단계를 건너뛸 수 있어요.
비워 둔 단계를 자식이 빠뜨리면 어떻게 될까요? 코코아를 만들면서 <곁들이기>를 깜빡해 볼게요. 있어야 한다로 비워 둔 단계라서 하리가 에러로 알려 줘요.
[음료]를 밑설계하자:
<만들기>를 만들자 ():
'나'의 <우리기>()를 실행하자
'나'의 <곁들이기>()를 실행하자
<우리기>가 있어야 한다 ()
<곁들이기>가 있어야 한다 ()
(참고: 코코아를 만들면서 <곁들이기>를 깜빡했어요)
[음료]를 바탕으로 [코코아]를 설계하자:
<우리기>를 만들자 ():
"코코아 가루를 녹여요."를 출력하자
새로운 [코코아]()의 <만들기>()를 실행하자
뼈대를 자식이 바꿔 버리면요?
하리에서는 자식이<만들기>를 덮어쓰는 것도 막지 않아요. 그러면 순서를 한곳에 모은 의미가 없어지니, 템플릿 메서드는 자식에서 덮어쓰지 않는다는 걸 팀의 약속으로 지켜 주세요. 이름에 “순서”가 드러나게 짓고, 위에(참고: 자식에서 덮어쓰지 마세요)라고 적어 두는 것도 좋아요.
새 음료를 더해 보면
“유자차”가 생겼다고 해 볼게요.
- 앞의 방식:
[유자차]에 네 단계 순서를 처음부터 다 적어야 해요. 물 끓이기와 따르기도 또 적어요. - 템플릿 메서드 패턴:
[음료]를 바탕으로 [유자차]를 만들고<우리기>와<곁들이기>두 개만 채우면 돼요. 순서도, 같은 단계도 부모가 해 줘요.
직접 해 보기
- 위 코드에
[유자차]를 더해 보세요. 우리기는 “유자청을 녹여요.”, 곁들이기는 “꿀을 넣어요.“예요.- 이번엔 순서를 바꿔 보세요. 모든 음료가 따르기 전에 “컵을 데워요.“를 하게 하려면, 어디를 고치면 될까요?
[음료]의<만들기>에 한 줄, 그리고<컵데우기>를 부모에 만들면 끝이에요. 자식 클래스는 하나도 안 고쳐요.
언제 쓰면 좋을까요?
- 여러 클래스가 같은 순서로 일하고, 몇 단계만 다를 때
- 순서를 한곳에서 지켜야 할 때 (빠뜨리면 안 되는 단계가 있을 때)
- 같은 단계가 여러 클래스에 복사되기 시작할 때
- 자식이 바꿔도 되는 곳과 안 되는 곳을 분명하게 나누고 싶을 때
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 순서가 한곳에만 있어서 바꾸기 쉬워요 | 상속을 쓰기 때문에, 자식은 부모를 하나만 고를 수 있어요 |
| 같은 단계를 복사하지 않아요 | 순서 자체가 음료마다 크게 다르면 맞지 않아요 |
| 새 자식은 다른 단계만 채우면 돼요 | 부모를 고치면 모든 자식이 영향을 받아요 |
| 갈고리로 단계를 골라서 바꿀 수 있어요 | 단계가 잘게 나뉘면 어디서 무엇을 하는지 따라가기 어려워요 |
너무 이른 패턴은 독이에요.
음료가 두 개뿐이고 늘어날 일이 없다면, 두 클래스가 각자 순서를 적는 것도 괜찮아요. 세 번째 음료를 만들면서 같은 순서를 또 복사하려는 순간이 부모로 올릴 때예요.
다른 패턴과의 관계
- 팩토리 패턴의 “팩토리 메서드”가 바로 이 패턴의 한 종류예요. 순서는 부모가 정하고, 그중 “무엇을 만들지” 한 단계만 자식이 채우는 거예요.
- 전략 패턴과 비교해 보세요. 템플릿 메서드는 상속으로 일부 단계를 바꾸고, 전략은 조합(객체를 끼우기)으로 방법 전체를 바꿔요. 실행 중에 바꿔 끼우고 싶다면 전략이, 순서를 단단히 지키고 싶다면 템플릿 메서드가 어울려요.
- 커맨드 패턴과 함께 쓸 수 있어요. 여러 명령이 “기록 남기기 → 실행 → 결과 알리기”처럼 같은 순서를 지켜야 한다면, 명령의 부모를 템플릿 메서드로 만들 수 있어요.