템플릿 메서드 패턴
템플릿 메서드 패턴은 핵심 기능과, 부가 기능을 분리하여 모듈화하는 디자인 패턴이다.
예제를 통해 템플리 메서드 패턴을 살펴보자.
이전 장에서 로그 추적기를 만들었다. 도입 전과 후의 코드를 비교해보자.
- 도입 전에는 핵심 기능만 남아 있는 반면, 도입 후에는 부가 기능이 훨씬 많고 복잡하다.
핵심 기능 vs 부가 기능
핵심 기능
은 객체가 제공하는 고유의 기능이다.orderService
는 주문이 핵심 기능이다.부가 기능
은 핵심 기능을 보조하기 위해 제공하는 기능이다. 예를 들면 로그, 트랜잭션 등을 들 수 있다.
TemplateMethodTest
@Slf4j
public class TemplateMethodTest {
@Test
void templateMethodV0() {
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직 1 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic2() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
log.info("비즈니스 로직 2 실행");
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
- 다음 코드를 살펴보면,
logic1()
과logic2()
는 시간을 측정하는 부분과 비즈니스 로직을 실행하는 부분이 함께 존재한다. - 이 코드를 템플릿 메서드 패턴에 맞게 분리하여 구현해보자.
템플릿 메서드 패턴 - 예제 1
템플릿 메서드 패턴은 이름 그대로 템플릿을 사용하는 것이다.
변하지 않는 부분은 몰아서 구현하고, 변하는 부분을 별도로 호출해서 해결한다.
AbstractTemplate
@Slf4j
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
call(); // 상속 받은 구현체에서 구현
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
protected abstract void call();
}
- 시간 측정 로직은 변하지 않는다. 따라서 하나의 템플릿으로 만들어둔다.
- 템플릿 안에서 변하는 비즈니스 로직을 자식이 상속 받아 구현하도록 처리한다.
SubClass
@Slf4j
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직 1 실행");
}
}
@Slf4j
public class SubClassLogic2 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
}
- 변하는 부분인
call()
을 자식이 상속 받아 구현하도록 처리했다.
TemplateMethodTest - templateMethodV1() 테스트
@Test
void templateMethodV1() {
AbstractTemplate template1 = new SubClassLogic1();
template1.execute();
AbstractTemplate template2 = new SubClassLogic2();
template2.execute();
}
- 템플릿 메서드 패턴으로 구현한 코드를 실행해보자.
- 다음과 같이 구현체 별로 실행 시간을 측정하는 로직이 정상 실행된다.
템플릿 메서드 패턴 - 예제 2
예제 1을 살펴보면 그럴싸하게 템플릿 메서드를 적용한 것 같다.
하지만 필요할 때 마다 구현체 클래스를 만드는 것은 큰 단점으로 작용할 수 있다.
익명 내부 클래스 사용하기
익명 내부 클래스를 사용하면 클래스 생성에 단점을 보완할 수 있다.
참고로 익명 내부 클래스란, 이름이 없고 클래스 내부에 선언 되는 클래스를 의미한다.
TemplateMethodTest - templateMethodV2() 추가
@Test
void templateMethod2() {
AbstractTemplate template1 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직 1 실행");
}
};
log.info("클래스 이름1={}", template1.getClass());
template1.execute();
AbstractTemplate template2 = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
};
log.info("클래스 이름2={}", template2.getClass());
template2.execute();
}
- 익명 내부 클래스를 사용해서도 동일한 결과를 얻을 수 있다.
- 익명 내부 클래스는
getClass()
를 했을 때 이름이 없기 때문에 자바에서 임의로 만든TemplateMethodTest$1
과 같이 출력 된다.
템플릿 메서드 패턴 - 적용 1
이전에 만든 로그 추적기에 템플릿 메서드 패턴을 적용해보자.
AbstractTemplate
@Slf4j
public abstract class AbstractTemplate<T> {
private final LogTrace trace;
public AbstractTemplate(LogTrace trace) {
this.trace = trace;
}
public T execute(String message) { // 변하지 않는 로직
TraceStatus status = null;
try {
status = trace.begin(message);
T result = call();
trace.end(status);
return result;
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
protected abstract T call(); // 자식이 상속 받아 구현할 것
}
AbstractTemplate
은 템플릿 메서드 패턴에서 부모 클래스이고, 템플릿 역할을 하고 있다.<T>
제네릭을 사용하여 타입 세이프티하도록 변경하였다. 참고로 반환타입이 없는 경우java.lang.Void
타입을 쓰면 된다.
OrderControllerV4 변경 전 후
- 변경한 템플릿 메서드 덕에 코드가 간결해졌다.
OrderServiceV4 변경 전 후
OrderRepositoryV4 변경 전 후
좋은 설계란
템플릿 메서드 패턴을 적용하여 변하는 코드와 변하지 않는 코드를 명확하게 분리했다.
좋은 설계란 여러 정의가 있지만, 변경이 일어날 때 간단하게 처리되는 것이 좋은 설계의 대표적인 예로 볼 수 있다.
GOF 디자인 패턴에는 템플릿 메서드를 다음과 같이 정의했다.
"작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다.
템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있다."
본 포스팅은 [인프런] - 스프링 핵심 원리 고급편을 참고하여 포스팅하였습니다.