템플릿 메서드 패턴
의도
템플릿 메서드는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴입니다.
문제
회사 문서들을 분석하는 데이터 마이닝 앱을 만들고 있다고 가정해 봅시다. 사용자들은 앱에 다양한 형식(PDF, DOC, CSV)의 문서들을 제공하고 앱은 이러한 문서들에서 일관된 형식으로 의미 있는 데이터를 추출하려고 시도합니다.
앱의 첫 번째 버전은 DOC 파일과만 작동할 수 있었고, 다음 버전에서는 CSV 파일을 지원할 수 있었습니다. 한 달 후, 당신은 앱이 PDF 파일에서 데이터를 추출하도록 '가르쳤습니다'.
어느 날 당신은 세 클래스 모두에 유사한 코드가 많다는 것을 알게 되었습니다. 다양한 데이터 형식들을 처리하는 코드는 클래스마다 완전히 다르지만 데이터 처리 및 분석을 위한 코드는 거의 같습니다. 알고리즘 구조는 그대로 두되, 코드 중복은 제거하는 게 좋지 않을까요?
이 클래스들을 사용하는 클라이언트 코드와 관련된 또 다른 문제가 있었습니다. 이 코드에는 작업을 처리하고 있는 클래스에 따라 적절한 행동들을 선택하는 조건문이 많이 있었습니다. 세 처리 클래스에 전부 공통 인터페이스나 공통 기초 클래스가 있었다면, 클라이언트 코드에서 조건문들을 제거하고 처리 객체에 메서드를 호출할 때 다형성을 사용할 수 있었을 겁니다.
해결책
템플릿 메서드 패턴은 알고리즘을 일련의 단계들로 나누고, 이러한 단계들을 메서드들로 변환한 뒤, 단일 템플릿 메서드 내부에 이러한 메서드들에 대한 일련의 호출들을 넣으라고 제안합니다. 이러한 단계들은 abstract
(추상)이거나 일부 디폴트(기본값) 구현을 가질 것입니다. 알고리즘을 사용하기 위해 클라이언트는 자신의 자식 클래스를 제공해야 하고, 모든 추상 단계를 구현해야 하며, 필요하다면 (템플릿 메서드를 제외한) 선택적 단계 중 일부를 오버라이드(재정의)해야 합니다.
이것이 당신의 데이터 마이닝 앱에서 어떻게 작동하는지 봅시다. 세 가지 파싱 알고리즘들 모두를 위한 기초 클래스를 만들 수 있습니다. 이 기초 클래스는 다양한 문서 처리 단계들에 대한 일련의 호출들로 구성된 템플릿 메서드를 정의합니다.
처음에는 모든 단계를 abstract
로 선언하여 자식 클래스들이 이러한 메서드들에 대한 자체 구현을 제공하도록 강제할 수 있습니다. 당신의 앱의 경우, 자식 클래스들은 이미 필요한 모든 구현들을 가지고 있으므로, 우리가 해야 할 유일한 일은 메서드들의 시그니처들을 부모 클래스의 메서드들과 일치하도록 조정하는 것입니다.
이제 중복 코드를 제거하기 위해 무엇을 할 수 있는지 봅시다. 파일 열기/닫기 및 데이터 추출/파싱을 위한 코드는 데이터 형식들에 따라 다르므로 해당 메서드들을 건드릴 의미가 없습니다. 그러나 미가공 데이터 분석 및 보고서 작성과 같은 다른 단계들의 구현은 매우 유사하므로 기초 클래스로 끌어올릴 수 있습니다. 그러면 자식 클래스들은 기초 클래스에서 이 코드를 공유할 수 있습니다.
보시다시피 두 가지 유형의 단계들이 있습니다:
- 모든 자식 클래스는 추상 단계들을 구현해야 합니다.
- 선택적 단계들에는 이미 어떤 디폴트(기본값) 구현이 있지만, 필요한 경우 이를 무시하고 오버라이드(재정의) 할 수 있습니다.
훅이라는 또 다른 유형의 단계가 있습니다. 훅은 몸체가 비어 있는 선택적 단계입니다. 템플릿 메서드는 훅이 오버라이드 되지 않아도 작동합니다. 일반적으로 훅들은 알고리즘의 중요한 단계들의 전 또는 후에 배치되어 자식 클래스들에 알고리즘에 대한 추가 확장 지점들을 제공합니다.
실제상황 적용
템플릿 메서드 접근 방식은 대량 주택 건설에 사용할 수 있습니다. 표준 주택 건설을 위한 건축 계획에는 잠재적 주택 소유자가 결과 주택의 일부 세부 사항들을 조정할 수 있도록 하는 여러 확장 지점들이 포함될 수 있습니다.
완성된 집이 다른 집들과 조금씩 다르도록 각 건축 단계(예: 기초 쌓기, 골조공사, 벽 쌓기, 수도 및 전기 배선 설치 등)는 약간씩 변경될 수 있습니다.
구조
-
추상 클래스는 알고리즘의 단계들의 역할을 하는 메서드들을 선언하며, 이러한 메서드를 특정 순서로 호출하는 실제 템플릿 메서드도 선언합니다. 단계들은
abstract
로 선언되거나 일부 디폴트 구현을 갖습니다. -
구상 클래스들은 모든 단계들을 오버라이드할 수 있지만 템플릿 메서드 자체는 오버라이드 할 수 없습니다.
의사코드
이 예시에서 템플릿 메서드 패턴은 간단한 전략 비디오 게임의 인공 지능의 다양한 브랜치들에 대한 '골격'을 제공합니다.
게임의 모든 종족은 거의 같은 유형의 유닛들과 건물들을 가지고 있습니다. 따라서 다양한 종족에 대해 같은 인공지능 구조를 재사용하면서 일부 세부 사항들은 오버라이드할 수 있습니다. 이 접근 방식을 사용하면 오크들의 인공지능을 오버라이드하여 그들을 더 공격적으로 만들고, 같은 방식으로 인간들을 방어 지향적으로 만들고 몬스터들은 아무것도 건설할 수 없도록 만들 수 있습니다. 게임에 새 종족을 추가하려면 새 인공지능 자식 클래스를 만들고 기초 인공지능 클래스에 선언된 디폴트 메서드들을 오버라이드해야 합니다.
적용
템플릿 메서드 패턴은 클라이언트들이 알고리즘의 특정 단계들만 확장할 수 있도록 하고 싶을 때, 그러나 전체 알고리즘이나 알고리즘 구조는 확장하지 못하도록 하려고 할 때 사용하세요.
템플릿 메서드는 모놀리식 알고리즘을 일련의 개별 단계들로 전환할 수 있도록 합니다. 이 알고리즘은 부모 클래스에서 정의된 구조를 그대로 유지하면서 자식 클래스들에 의해 쉽게 확장될 수 있습니다.
이 패턴은 약간의 차이가 있지만 거의 같은 알고리즘들을 포함하는 여러 클래스가 있는 경우에 사용하세요. 결과적으로 알고리즘이 변경되면 모든 클래스를 수정해야 할 수도 있습니다.
이러한 알고리즘을 템플릿 메서드로 전환하면 유사한 구현들이 있는 단계들을 부모 클래스로 끌어올릴 수 있으며, 그로 인해 코드 중복을 제거할 수 있습니다. 자식 클래스 중 서로 코드가 다른 부분들은 자식 클래스들에 남겨놓을 수 있습니다.
구현방법
-
대상 알고리즘을 분석하여 여러 단계로 나눌 수 있는지 확인하세요. 어떤 단계들이 모든 자식 클래스에 공통인지 또 어떤 단계들이 항상 고유한지를 고려하세요.
-
추상 기초 클래스를 만들고 알고리즘의 단계들을 표현하는 템플릿 메서드와 추상 메서드들의 집합을 선언하세요. 해당하는 단계들을 실행하여 템플릿 메서드에서 알고리즘의 구조의 윤곽을 잡으세요. 템플릿 메서드를
final
로 만들어 자식 클래스들이 메서드를 오버라이드하지 못하도록 하는 것을 고려하세요. -
모든 단계가 추상적이어도 괜찮습니다. 그러나 일부 단계들에는 디폴트 구현이 있는 것이 도움이 될 수 있습니다. 자식 클래스들은 이러한 디폴트 메서드들을 구현할 필요가 없습니다.
-
알고리즘의 중요한 단계들 사이에 훅들을 추가하는 것을 고려하세요.
-
알고리즘의 각 변형에서 새로운 구상 자식 클래스를 생성하세요. 새로운 구상 자식 클래스는 모든 추상 단계들을 반드시 구현해야 하지만 일부 선택 단계를 오버라이드할 수도 있습니다.
장단점
- 클라이언트들이 대규모 알고리즘의 특정 부분만 오버라이드하도록 하여 그들이 알고리즘의 다른 부분에 발생하는 변경에 영향을 덜 받도록 할 수 있습니다.
- 중복 코드를 부모 클래스로 가져올 수 있습니다.
- 일부 클라이언트들은 알고리즘의 제공된 골격에 의해 제한될 수 있습니다.
- 당신은 자식 클래스를 통해 디폴트 단계 구현을 억제하여 리스코프 치환 원칙을 위반할 수 있습니다.
- 템플릿 메서드들은 단계들이 더 많을수록 유지가 더 어려운 경향이 있습니다.
다른 패턴과의 관계
-
팩토리 메서드는 템플릿 메서드의 특수화라고 생각할 수 있습니다. 동시에 대규모 템플릿 메서드의 한 단계의 역할을 팩토리 메서드가 할 수 있습니다.
-
템플릿 메서드는 상속을 기반으로 합니다. 이 메서드는 자식 클래스들에서 알고리즘의 부분들을 확장하여 변경할 수 있도록 합니다. 전략 패턴은 합성을 기반으로 합니다: 당신은 객체 행동의 일부분들을 이러한 행동에 해당하는 다양한 전략들을 제공하여 변경할 수 있습니다. 템플릿 메서드는 클래스 수준에서 작동하므로 정적입니다. 전략 패턴은은 객체 수준에서 작동하므로 런타임에 행동들을 전환할 수 있도록 합니다.