템플릿 메서드를 GOF 디자인 패턴에서는 다음과 같이 정의한다.
템플릿 메서드 디자인 패턴의 목적은 다음과 같습니다. "작업에서 알고리즘의 골격을 정의하고 일 부 단계를 하위 클래스로 연기합니다.템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변겨하지 않고도 알고리즘의 특정 단계를 재정의 할 수 있습니다. (GOF)"
추상 클래스 A에서 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의하는 것이다. 이렇게 하면 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고 특정 부분만 재정의할 수 있다. 결국 상속과 오버라이딩을 통한 다형성으로 문제를 해결하는 것이다.
템플릿 메서드 패턴은 결국 상속을 이용하는 것이기 때문에 상속에서 오는 단점을 그대로 안고있다. 특히 자식 클래스가 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있다.
강하게 결합되는 것이 왜 문제일까? 상속을 받는 다는 것은 부모의 기능을 자식이 똑같이 기능한다는 것인데 만약 자식이 부모의 기능을 아무것도 사용하지 않는다면 이는 의미없는 상속일 것이다. 이것은 좋은 설계가 아니다.
또한 템플릿 메서드 패턴은 상속 구조를 사용하기 때문에, 별도의 클래스나 익명 내부 클래스를 만들어야 하는 부분도 복잡하다.
이러한 상속의 단점을젝할 수 있는 디자인 패턴이 전략 패턴이다.
비즈니스 로직에 로그를 남기려고한다.
@Service
@RequiredArgsConstructor
public class TeamServiceWithTemplateMethodPattern {
private final TeamRepository teamRepository;
public Team save(Team team) {
long startTime = System.currentTimeMillis();
// 비즈니스 로직
Team savedTeam = teamRepository.save(team);
long endTime = System.currentTimeMillis();
System.out.println("수행 시간 : " + (endTime - startTime) + "ms");
return savedTeam;
}
}
이렇게 로직을 작성한다면 핵심 비즈니스 로직보다는 로그를 남기는 로직같은 부가 기능을 위한 코드가 더 많다. 이러한 부가 기능 코드를 효율적으로 작성하기 위해 템플릿 메서드 패턴을 적용해보자.
public abstract class AbstractTemplate<T> {
public abstract T call();
public T execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직
T result = this.call();
long endTime = System.currentTimeMillis();
System.out.println("수행 시간 : " + (endTime - startTime) + "ms");
return result;
}
}
위에서 말했듯이 템플릿 역할을 하게될 추상 메서드를 만들어서 핵심 비즈니스 로직을 추상 메서드를 오버라이드하여 재정의하고 부가기능을 템플릿에서 처리하도록 만들게 하였다.
@Service
@RequiredArgsConstructor
public class TeamServiceWithTemplateMethodPattern {
private final TeamRepository teamRepository;
public Team save(Team team) {
AbstractTemplate<Team> template = new AbstractTemplate<>() {
@Override
public Team call() {
return teamRepository.save(team);
}
};
return template.execute();
}
}
이렇게 템플릿 메서드 패턴을 이용하여 핵심로직만 재정의하여 사용하게 된다면 불필요하게 중복되는 코드를 줄일 수 있다. 이렇게 다형성을 이용하여 알고리즘 전체를 바꾸지 않고 핵심 특정 부분만 재정의하여 사용한다.
템플릿 메서드 패턴은 부가 기능 중복을 줄이고 핵심 기능에 집중할게 해준다. 그러나 이러한 핵심 기능 재정의를 모든 하위 클래스마다 해야하기 때문에 코드 복잡성이 올라가고, 사용하지 않는 부모 클래스의 메서드가 있을 수 있어 비효율적인 부분이 있을 수 있다. 따라서 적절히 상황에 맞게 사용해야한다.