- 참조: https://refactoring.guru/design-patterns/template-method
- 참조: GoF의 디자인 패턴
- 템플릿 메소드(Template Method)
템플릿 메소드 패턴은 부모 클래스에서 알고리즘의 뼈대를 정의하고, 서브 클래스에서 구조의 변화없이 알고리즘의 일부 단계를 재정의하는 패턴이다.
- 문제점
기업의 문서를 분석하는 데이터 마이닝 어플리케이션을 만든다고 가정해보자. 사용자는 다양한 포맷(PDF, DOC, CSV)의 문서를 사용하고, 여러 포맷에서 의미있는 데이터를 단일 포맷으로 추출한다.
처음에는 DOC 파일에서만 동작하게 만들었다면 그 다음 버전에서는 CSV 를 또 몇개월 후에는 PDF 에서도 동작이 되도록 점점 기능을 확장할 가능성이 크다.
기능을 확장하고 나면 3 개의 클래스들이 서로 비슷해질 가능성이 크다. 해당 클래스들의 코드들은 모두 다르지만, 데이터 처리와 분석에 관련된 코드는 거의 동일할 수 있다. 알고리즘 구조는 그대로 유지한채 중복된 코드를 제거하는편이 좋지 않을까?
문제는 여기서 그치지 않고 이런 클래스들을 사용하는 client 코드에도 나타난다. 객체를 처리하는 클래스에 따라 적절한 행동을 골라야하는 조건부 코드가 많아진다. 만약 3 개의 처리 클래스가 공통 인터페이스나 기본 클래스를 갖게되면, 클라이언트 코드에서 조건부 코드를 제거할 수 있고, 처리 객체의 메소드를 호출할 때 다형성을 사용할 수 있다.
- 해결책
템플릿 메소드 패턴은 알고리즘을 여러 단계로 나누고, 이 단계들에 해당하는 메소드들을 단일 template method 안에서 호출한다. 정의한 단계들은 abstract 로 선언되거나 기본 구현을 제공할것이다. 알고리즘을 사용하기 위해서 클라이언트는 서브 클래스를 제공하고, 서브 클래스들은 모든 추상 단계들을 구현하거나 필요하다면 일부 기본구현 메소드는 재정의할 수 있다. (단, template method 자체를 재정의 하면 안된다.)
데이터 마이닝앱에서 어떻게 동작하게 될지 살펴보자. 우선 3 개의 포맷에 대한 파싱 알고리즘의 기본 클래스를 만든다. 이 기본 클래스에서는 다양한 문서 처리 단계를 호출하는 template method 를 정의한다.
우선 모든 단계를 abstract 로 선언하고 서브 클래스들에게 이 메소드들을 구현하도록 강제한다. 우리 예제의 경우에 서브 클래스들은 이미 필요한 모든 구현을 가지고 있다고 가정하면, 부모 클래스의 메소드 시그니처와 매칭만 시키면 된다.
그 다음 중복 코드를 제거한다. 파일을 열거나 닫는것 그리고 파일에서 데이터를 추출하고, 이를 파싱하는것은 포맷에 따라 달라져야 하는 동작이라서 중복 코드를 추출할 여지가 없다. 하지만 raw 데이터를 분석하고 리포트를 제작하는 단계는 포맷이 달라져도 중복되는 부분이기 때문에 기본 클래스로 격상시킬 수 있다.
요악하면 1) 추상적인 단계들이 모든 서브클래스에서 구현 되어야 한다. 2) 선택적인 단계에는 일부 기본 구현이 있지만 필요하다면 오버라이드 한다.
2 가지 유형의 단계 말고도 "hooks" 라는 기능도 고려해볼 수 있다. 이 기능은 특별한 상황이 아니라면 별 내용이 없을것이다. 기본적으로는 hook 은 재정의 하지 않아도 잘 동작한다. 대게 hook 은 알고리즘의 중요 단계 전후에 위치하며, 서브클래스들에게 알고리즘 확장 포인트를 제공하기 위해 제공한다.
- 실생활 예제
Template method 는 주택 건축에 사용될 수 있다. 기성 주택을 짓는 설계에서 주택의 세부사항을 조정할 수 있도록 몇몇 확장 포인트를 제공하는 기능을 상상해보자. 기초를 다지거나, 프레임 설계, 벽 구축, 전기 및 수도 배관설치와 같은 각 건축단계에서 다른집들과는 약간 다르게 조정할 수 있을것이다.
- 구조
Template method 패턴의 구조를 알아보자.
- AbstractClass: AbstractClass 는 알고리즘의 각 단계를 정의하고 이와 더불어 이런 단계들을 특정 순서에 따라 호출하는 template method 를 정의한다. 이 단계들은 abstract 로 정의되거나 기본 동작을 구현한다.
- ConcreteClass: ConcreteClass 들은 이런 단계들을 구현하거나 재정의할 수 있지만 template method 자체는 재정의할 수 없다.
- 시나리오
이번 예제에서는 Template method 패턴을 적용하여 간단한 전략 비디오 게임에서 다양한 AI 의 동작을 구현해본다.
전략게임에서는 종족들이 거의 비슷한 유형의 유닛과 건물들을 갖고 있다. 그래서 여러 종족이라 할지라도 같은 AI 구조를 재사용할 수 있지만 세부사항은 재정의 되어야할 수 있다. 이런 접근법을 이용해서 오크의 AI 를 좀 더 공격적인 성향으로, 인간은 좀더 방어적인 성향, 몬스터는 건물을 짓지 못하게 재정의 할 수 있다. 게임에 새로운 종족을 추가하려면 새로운 AI 서브 클래스를 만들어야 하고, 기본 AI 클래스에 정의된 메소드들도 구현하거나 재정의 해야한다.
- Java 예제
우선 기본 클래스에 해당하는 GameAI 부터 정의해보자. 자신의 턴 마다 자원을 모으고, 건물을 짓고, 유닛을 생산하고 공격하는 takeTurn 메소드는 template method 에 해당한다. 이 메소드는 앞에서 설명한것과 같이 모든 세부 클래스들이 따라야 하는 일종의 절차의 뼈대라고 볼 수 있겠다.
attack과 collectResources 는 기본 구현 메소드에 해당한다. 이 메소드는 필요에 따라 세부사항이 다르다면 세부 클래스에서 재정의할 수 있는 대상이 된다.
나머지 abstract 메소드들은 세부 클래스에서 재정의해야 한다.
public abstract class GameAI {
private int x = -1, y = -1;
public void setCoordi(int x, int y) {
this.x = x;
this.y = y;
}
public void takeTurn() {
collectResouces();
buildStructures();
buildUnits();
attack();
}
public void collectResouces() {
System.out.println("GameAI Collect resources");
}
public abstract void buildStructures();
public abstract void buildUnits();
public void attack() {
if(x == -1 || y == -1) {
return;
}
sendScouts(x, y);
sendWarriors(x, y);
}
public abstract void sendScouts(int x, int y);
public abstract void sendWarriors(int x, int y);
}
먼저 오크 AI 에 해당하는 클래스부터 살펴보자. 오크들은 건물을 짓고 유닛을 생산한다. 그리고 정찰과 공격도 공격적으로 수행한다.
public class OrcsAI extends GameAI{
@Override
public void buildStructures() {
System.out.println("Orcs Build structures");
}
@Override
public void buildUnits() {
System.out.println("Orcs Build units");
}
@Override
public void sendScouts(int x, int y) {
System.out.println("Orcs send aggresive scouts: " + x + ", " + y);
}
@Override
public void sendWarriors(int x, int y) {
System.out.println("Orcs send aggresive warriors: " + x + ", " + y);
}
}
반면 몬스터 AI는 자원도 모으지 않으며, 건물을 짓거나 유닛도 생산하지 않는다. 단지 정찰을 하거나 공격을 할 뿐이다.
public class MonstersAI extends GameAI{
@Override
public void collectResouces() {
System.out.println("Monster don't collect resources");
}
@Override
public void buildStructures() {
System.out.println("Monster don't build structures");
}
@Override
public void buildUnits() {
System.out.println("Monster don't build units");
}
@Override
public void sendScouts(int x, int y) {
System.out.println("Monster send scout: " + x + ", " + y);
}
@Override
public void sendWarriors(int x, int y) {
System.out.println("Monster send warriors: " + x + ", " + y);
}
}
이제 Test를 해보자. 오크와 몬스터는 번갈아 가면서 턴을 수행한다. OrcsAI 는 GameAI 에서 기본으로 구현하는 자원수집을 사용하였고, 자신이 구현한 나머지 추상 메소드들도 잘 호출되었다. MonstersAI 또한 자원을 모으지 않는 것을 표현한 재정의 메소드가 잘 호출되었다.
public static void main(String[] args) {
GameAI orcsAI = new OrcsAI();
GameAI monstersAi = new MonstersAI();
System.out.println("1. Orcs turn====");
orcsAI.takeTurn();
System.out.println("2. monster turn====");
monstersAi.takeTurn();
System.out.println("3. Orcs turn====");
orcsAI.setCoordi(0, 100);
orcsAI.takeTurn();
System.out.println("4. monster turn====");
monstersAi.setCoordi(50, 50);
monstersAi.takeTurn();
}
==========================
1. Orcs turn====
GameAI Collect resources
Orcs Build structures
Orcs Build units
2. monster turn====
Monster don't collect resources
Monster don't build structures
Monster don't build units
3. Orcs turn====
GameAI Collect resources
Orcs Build structures
Orcs Build units
Orcs send aggresive scouts: 0, 100
Orcs send aggresive warriors: 0, 100
4. monster turn====
Monster don't collect resources
Monster don't build structures
Monster don't build units
Monster send scout: 50, 50
Monster send warriors: 50, 50
'Concepts > Design Pattern' 카테고리의 다른 글
행동 패턴 - 방문자(Visitor) (1) | 2021.06.20 |
---|---|
행동 패턴 - 전략(Strategy) (0) | 2021.06.20 |
행동 패턴 - 상태(State) (0) | 2021.06.19 |
행동 패턴 - 옵저버(Observer) (0) | 2021.06.17 |
행동 패턴 - 메멘토(Memento) (0) | 2021.06.13 |
댓글