템플릿 콜백 패턴
콜백(callback)이란 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 의미한다.
콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행하거나 나중에 실행할 수 있다. (call - 호출은 되었고, back - 실행은 뒤에서)
@Test
void strategyV2() {
ContextV2 context = new ContextV2();
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
context.execute(new Strategy() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
}
전략 패턴에서 컨텍스트에 전략을 파라미터로 넘겨주는 방식을 템플릿 콜백
패턴이라 부른다.
Context
가 템플릿 역할을 하고, Strategy
부분이 콜백으로 넘어온다고 생각하면 된다.
템플릿 콜백 패턴은 GoF 패턴은 아니고, 스프링 내부에서 자주 사용되기 때문에 부르게 된 패턴이다.
JdbcTemplate
,TransactionTemplate
등이 템플릿 콜백 패턴이다.
템플릿 콜백 패턴 - 예제
템플릿 콜백 패턴을 구현해보자.
전략 패턴에서 Context(변하지 않는)
가 Template(변하는 알고리즘)
이 되고, Strategy
가 Callback
이 된다.
Callback 인터페이스
public interface Callback {
void call();
}
Strategy
에 해당하는 콜백 인터페이스를 생성하자. 인터페이스를 통해 구현체를 유연하게 만들 수 있도록 하자.
TimeLogTemplate
@Slf4j
public class TimeLogTemplate {
public void execute(Callback callback) {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
callback.call(); // 위임
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
Context
에 해당하는 고정된 로직을 넣고, 파라미터로 받은 콜백을 실행해주자.Callback
을 필드에 선언해두어도 되지만, 템플릿과 콜백간의 유연한 설계를 위해 파라미터로 넣는다.
TemplateCallbackTest
@Test
void callbackV2() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(() -> log.info("비즈니스 로직 1 실행"));
template.execute(() -> log.info("비즈니스 로직 2 실행"));
}
템플릿 콜백 패턴 - 적용
로그 추적기 애플리케이션에 템플릿 콜백 패턴을 적용해보자.
TraceCallback 인터페이스
public interface TraceCallback<T> {
T call();
}
TraceTemplate
public class TraceTemplate {
private final LogTrace trace;
public TraceTemplate(LogTrace trace) {
this.trace = trace;
}
public <T> T execute(String message, TraceCallback<T> callback) {
TraceStatus status = null;
try {
status = trace.begin(message);
// 로직 호출
T result = callback.call();
trace.end(status);
return result;
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
<T>
제네릭을 사용하여 타입 세이프티하게 템플릿을 만들자.LogTrace
는 로그 시작과 끝, 예외 등을 기록하는 인터페이스로 템플릿 생성자에 파라미터로 넣어 생성하도록 하자.
- Log 와 관련된 코드는 다음과 같이 구성되어 있다.
OrderControllerV5
- 컨트롤러의 필드에
TraceTemplate
을 추가하고, 템플릿에LogTrace
를 파라미터로 넣어주도록 변경하였다. ( TraceTemplate을 빈으로 등록하여 사용해도 된다.) TraceTemplate
에는 콜백에 해당하는call()
메서드를 구현하기 위해 람다를 사용하여 처리했다.
OrderServiceV5
- 컨트롤러와 마찬가지로
TraceTemplate
적용을 위해 코드를 수정하자.
OrderRepositoryV5
- 레포지토리도 동일하게 변경하자.
실행 결과 로그
2023-02-08 23:51:00.934 INFO 1849 --- [nio-8080-exec-1] h.a.trace.logtrace.ThreadLocalLogTrace : [cda5e30e] OrderController.request()
2023-02-08 23:51:00.939 INFO 1849 --- [nio-8080-exec-1] h.a.trace.logtrace.ThreadLocalLogTrace : [cda5e30e] |-->OrderService.request()
2023-02-08 23:51:00.939 INFO 1849 --- [nio-8080-exec-1] h.a.trace.logtrace.ThreadLocalLogTrace : [cda5e30e] | |-->OrderRepository.save()
2023-02-08 23:51:01.950 INFO 1849 --- [nio-8080-exec-1] h.a.trace.logtrace.ThreadLocalLogTrace : [cda5e30e] | |<--OrderRepository.save() time=1011ms
2023-02-08 23:51:01.951 INFO 1849 --- [nio-8080-exec-1] h.a.trace.logtrace.ThreadLocalLogTrace : [cda5e30e] |<--OrderService.request() time=1015ms
2023-02-08 23:51:01.951 INFO 1849 --- [nio-8080-exec-1] h.a.trace.logtrace.ThreadLocalLogTrace : [cda5e30e] OrderController.request() time=1017ms
- 기존에 사용하던 로그 추적기가 정상적으로 출력 된다.
정리
변하는 코드와 변하지 않는 코드를 분리하기 위해, 템플릿 콜백 패턴을 적용하였다.
하지만 최적화를 해도, 결국 원본 코드를 수정해야 된다는 한계가 있다.
개발자의 게으름에 대한 욕심을 바탕으로 프록시를 기반으로 원본 코드에 손대지 않고 적용하는 방법을 만들어냈다.
앞으로 XxxTemplate
을 본다면, 템플릿 콜백 패턴을 떠올리자.
본 포스팅은 인프런 - 스프링 핵심 원리 - 고급편을 참고하여 포스팅하였습니다.