비슷한 알고리즘이 여러 클래스에 중복되어 있을때, 알고리즘(문제를 푸는 방법)을 뼈대를 정의하고 일부를 서부 클래스로 위임하여 중복을 해소하는 패턴이다.
🫧 Stragety 패턴과의 차이점?
전략 패턴은, 바뀌는 부분인 알고리즘의 전체를 캡슐화해서 넘겨 동적으로 변경하는 패턴이다.
반면, 템플릿 메소드는 알고리즘 구조를 변경하지 않고 변경되는 알고리즘의 일부 내용(단계)만을 서브 클래스에서 재정의할 수 있도록 한다.
이렇게 되면 알고리즘의 중복되는 부분을 부모 클래스에 캡슐화시키 때문에 중복 코드를 줄일 수 있어 유지보수에 유리하다는 장점이 존재한다.
또한, 알고리즘 뼈대가 존재하는 고수준 구성 요소에서 메서드 재정의가 필요한 경우에만 하위 클래스인 저수준 구성 요소를 호출하기 때문에 의존관계가 복잡해지지 않는다.
abstract class AbstractClass {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation();
// concreteOperation() 메소드 코드
}
void hook() {}
}
primitiveOperation()
: 서브 클래스에 알고리즘의 일부 동작을 위임하는 부분concreteOperation()
: 위임하지 않는 부분알고리즘에서 변하는 특정 단계를 추상 메서드로 정의하고, 서브클래스에서 적절하게 구현을 처리하도록 한다. 이렇게 되면, 부모 클래스를 사용하는 클라이언트 입장에서 서브 클래스 구현이 변경되어도 영향이 가지 않는다.
변경되지 않는 공통 영역은 오버라이딩을 금지하기 위해 final
로 선언하는 것이 좋다.
🫧 hook 메서드
서브클래스가 꼭 구현하지 않고 선택적으로 추상 클래스에서 진행되는 작업을 처리할지 말지 결정할 수 있는 메서드이다.
두 개의 방법에는 비슷한 점들이 존재한다. (ex) 물을 끓인다, 끓는 물에 우려낸다, 음료를 컵에 따른다, 데코를 추가한다)
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("설탕과 우유를 추가하는 중");
}
}
Coffea
와 Tea
가 각각 두 클래스에서 각자 알고리즘을 수행하게 되므로, 두 클래스에 중복된 코드가 생겨난다.
즉, 알고리즘에 변경이 일어나면 클래스를 여러 곳에서 고쳐야하는 불편함이 생기는 것이다. 이는 유지보수에 큰 타격을 준다. 앞에서 보았던 템플릿 메소드 패턴을 적용해보자!
public abstract class CaffeineBeverage {
public final void prepareRecipe() { //템플릿 메소드
boilWater();
brew();
pourInCup();
addCondiments();
}
public abstract void brew();
public abstract void addCondiments();
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
prepareRecipe()
는 템플릿 메소드로, 알고리즘의 틀을 제공한다. 이로써 중복되는 코드는 부모 클래스로 옮겨놓고, 서브 클래스에서는 달라지게 되는 부분들의 코드만 넣어놓으므로써 알고리즘을 한 군데로 모아 변경이 되는 지점을 최소화시켰다.
class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("차를 우려내는 중");
}
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
Arrays.sort()
에서 사용하는 mergeSort()
는 정렬 알고리즘(골격)을 정의한 메서드로 템플릿 메서드 패턴을 사용한다. 두 원소의 위치를 바꾸는 행동을 결정짓는 대소 관계 결과를 체크하는 부분을 서브 클래스에 위임하고 있기 때문이다.
compareTo
함수는 서브 클래스에서 재정의해서 제공해줘야 한다.하지만 앞에서 보았던 패턴과 다르게 변하는 부분이 추상 메서드로 정의되어 있는 것도 아니고, Arrays
의 서브 클래스를 만드는 것이 아니므로 서브 클래스의 compareTo()
메서드를 재정의를 했는지 따로 확인하는 과정이 필요해진다.
public interface Comparable<T> {
public int compareTo(T o);
}
해당 문제를 해결하기 위해 Comparable
인터페이스가 도입되었고, Arrays
의 서브 클래스를 만드는 대신 배열의 원소가 Comparable
인터페이스 구현을 통해 알고리즘을 완성시킬 수 있었다.
굳이 디자인 패턴의 정해진 형식을 지키는 것이 중요한게 아니라, 패턴의 기본 정신과 용도를 충실히 따르기만 하면 된다. 형식은 주어진 상황과 구현상 제약조건에 따라 수정될 수 있다.
템플릿 메소드는 상속을 이용해서 알고리즘의 일부를 다른 클래스에 맡긴다. 즉, 알고리즘 자체가 서브 클래스가 없으면 동작되지 않아 불완전하다.
🫧 팩토리 메서드와 템플릿 메서드 패턴
팩토리 메서드도 일종의 특화된 템플릿 메서드 패턴이다. 다양한 객체를 생성하는 달라지는 부분만을 추상 클래스로 정의하고, 서브 클래스들에 구현을 위임하였기 때문이다. 그리고 추가적으로 생성되는 타겟인 객체는 구성 방식을 통해 가지고 있어서 의존성 역전 원칙도 동시에 지키는 패턴이다.
스프링에서 템플릿 메서드 패턴은 요청을 처리하는 과정에서 볼 수 있다.
/**
Process this request, publishing an event regardless of the outcome.
The actual event handling is performed by the abstract
{@link #doService} template method.
/
FrameworkServlet
의 processRequest
에서 요청을 처리하는 알고리즘의 골격을 짜두었고, 이를 상속받은 DispatcherServlet
에서 doService()
추상 클래스를 구현하였다.
그리고 DispatcherServlet
의 doService
의 doDispatch
메서드에서는 우리가 아는 핸들러와 핸들러 어댑터를 찾고 수행시켰다가 뷰로 렌더링 하는 디스패치 과정이 일어난다.