참고자료
템플릿 콜백 패턴 - 시작
템플릿 콜백 패턴은 굉장히 자주 사용되는 패턴이다. 이전 포스팅에서 배웠던 전략 패턴에서 ContextV2는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온 Strategy의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백이라고 한다.
프로그래밍에서 콜백(callback) 또는 콜애프터 함수는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다. 좀 더 직관적으로 말하면 파라미터로 실행 가능한 코드를 넘겨주면 받는 쪽에서 그 코드를 실행하는 것이고, 이 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
근데 자바에서는 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다. 자바8 이전에는 하나의 메소드를 가진 인터페이스를 구현하고 주로 익명 내부 클래스를 사용했다. 자바8 이후로 람다를 사용할 수 있기 때문에 최근에는 주로 람다를 사용한다.
템플릿 콜백 패턴
- 스프링에서는 이전 포스팅에서 본 ContextV2와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라고 한다. 전략 패턴에서 Context가 템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다 생각하면 된다. 그러니까 이전에 봤던 ContextV2과 완벽하게 템플릿 콜백 패턴이라고 생각하면 된다.
- 참고로, 템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에 스프링 안에서만 이렇게 부른다. 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라 생각하면 된다.
- 스프링에서는 JdbcTemplate, RestTemplate, TransactionTemplate, RedisTemplate처럼 다양한 템플릿 콜백 패턴이 사용된다. 스프링에서 이름에 xxxTemplate이 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.
템플릿 콜백 패턴 - 예제
템플릿 콜백 패턴을 구현해보자. ContextV2와 내용이 같고 이름만 다르다고 보면 된다.
- Context → Template
- Strategy → Callback
Callback
package cwchoiit.springadvanced.trace.strategy.code.template;
@FunctionalInterface
public interface Callback {
void call();
}
TimeLogTemplate
package cwchoiit.springadvanced.trace.strategy.code.template;
import lombok.extern.slf4j.Slf4j;
@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 = {}ms", resultTime);
}
}
- execute()는 파라미터로 Callback 객체를 받는다. (자바는 실행 가능한 코드를 인수로 넘기려면 객체를 전달해야 한다 했다.)
- 받은 객체가 가지고 있는 call()을 실행한다.
TemplateCallbackTest
package cwchoiit.springadvanced.trace.strategy;
import cwchoiit.springadvanced.trace.strategy.code.template.TimeLogTemplate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class TemplateCallbackTest {
@Test
void callbackV1() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(() -> log.info("비즈니스 로직1 실행"));
TimeLogTemplate template2 = new TimeLogTemplate();
template2.execute(() -> log.info("비즈니스 로직2 실행"));
}
}
- 템플릿 콜백 패턴을 테스트할 코드다. 콜백함수를 실행할 TimeLogTemplate 객체를 만든다. 이 객체가 가지고 있는 execute()는 파라미터로 Callback 객체를 받는다. Callback 객체를 전달하기 위해 익명 내부 클래스로 전달하거나, 람다로 전달하면 된다. 물론 별도의 클래스를 따로 만들어서 전달해도 무방하다. 재사용성이 빈번한 경우 그게 더 효율적이다. 그러나 그게 아니라면 위와 같이 람다를 사용하는 것이 좀 더 편리할 것 같다.
실행 결과
16:44:54.855 [Test worker] INFO com.example.advanced.trace.strategy.TemplateCallbackTest -- 비즈니스 로직1 실행
16:44:54.862 [Test worker] INFO com.example.advanced.trace.strategy.code.template.TimeLogTemplate -- resultTime=9
16:44:54.865 [Test worker] INFO com.example.advanced.trace.strategy.TemplateCallbackTest -- 비즈니스 로직2 실행
16:44:54.865 [Test worker] INFO com.example.advanced.trace.strategy.code.template.TimeLogTemplate -- resultTime=0
템플릿 콜백 패턴 - 적용
이제 템플릿 콜백 패턴을 애플리케이션에 적용해보자. LogTrace를 사용하는 것 말이다.
TraceCallback
package cwchoiit.springadvanced.trace.templatecallback;
@FunctionalInterface
public interface TraceCallback<T> {
T call();
}
- 콜백을 전달하는 인터페이스이다.
- <T> 제네릭을 사용했다. 콜백의 반환 타입을 정의한다.
TraceTemplate
package cwchoiit.springadvanced.trace.templatecallback;
import cwchoiit.springadvanced.trace.TraceStatus;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class TraceTemplate {
private final LogTrace 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;
}
}
}
- 콜백 함수를 받아 실행할 템플릿 클래스이다.
- 콜백 함수를 받는 execute()는 공통 부분인 로그 추적 코드가 있고 그 사이에 콜백 함수를 실행한다. 콜백 함수의 반환값이 execute()의 반환값이 되고 그 반환값이 제네릭이므로 메서드 또한 제네릭이다.
OrderControllerV5
package cwchoiit.springadvanced.app.v5;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import cwchoiit.springadvanced.trace.templatecallback.TraceTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderControllerV5 {
private final OrderServiceV5 orderService;
private final TraceTemplate traceTemplate;
public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
this.traceTemplate = new TraceTemplate(trace);
this.orderService = orderService;
}
@GetMapping("/v5/request")
public String request(String itemId) {
return traceTemplate.execute("OrderControllerV1.request(String itemId)", () -> {
orderService.orderItem(itemId);
return "ok";
});
}
}
- TraceTemplate을 주입 받는 방법은 다양하지만, 여기서는 생성자에서 new를 선언해서 인스턴스를 만든다. 그리고 필요한 LogTrace를 받아야한다.
- 물론, 이렇게 말고 TraceTemplate을 스프링 빈으로 아예 등록해서 주입받는 방법도 있다. 이렇게 하면 이 TraceTemplate도 딱 하나의 싱글톤 객체로 만들어진다.
- 장점과 단점이 있는데, 위 코드처럼 하면 장점은 테스트 코드 작성이 매우 수월해진다는 것이다. 테스트 코드를 작성할 때 TraceTemplate을 빈으로 등록해서 스프링 컨테이너에 추가할 필요가 없으니까. 단점은 이 클래스의 개수만큼 TraceTemplate이 객체로 생성된다는 점이다.
- 템플릿 콜백 패턴을 사용했더니 코드가 더 더 깔끔해졌다.
OrderServiceV5
package cwchoiit.springadvanced.app.v5;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import cwchoiit.springadvanced.trace.templatecallback.TraceTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderServiceV5 {
private final OrderRepositoryV5 orderRepository;
private final TraceTemplate traceTemplate;
public OrderServiceV5(OrderRepositoryV5 orderRepository, LogTrace trace) {
this.orderRepository = orderRepository;
this.traceTemplate = new TraceTemplate(trace);
}
public void orderItem(String itemId) {
traceTemplate.execute("OrderServiceV1.orderItem(String itemId)", () -> {
orderRepository.save(itemId);
return null;
});
}
}
OrderRepositoryV5
package cwchoiit.springadvanced.app.v5;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import cwchoiit.springadvanced.trace.templatecallback.TraceTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class OrderRepositoryV5 {
private final TraceTemplate traceTemplate;
public OrderRepositoryV5(LogTrace trace) {
this.traceTemplate = new TraceTemplate(trace);
}
public void save(String itemId) {
traceTemplate.execute("OrderRepositoryV1.save(String itemId)", () -> {
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
return null;
});
}
private void sleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
정리
지금까지 우리는 변하는 코드와 변하지 않는 코드를 분리하고, 더 적은 코드로 로그 추적기를 적용하기 위해 고군분투했다.
템플릿 메서드 패턴, 전략 패턴, 그리고 템플릿 콜백 패턴까지 진행하면서 변하는 코드와 변하지 않는 코드를 분리했다. 그리고 최종적으로 템플릿 콜백 패턴을 적용하고 콜백으로 람다를 사용해서 코드 사용도 최소화 할 수 있었다.
한계
그런데 지금까지 설명한 방식의 한계는 아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점이다. 클래스가 수백개면 수백개를 더 힘들게 수정하는가 조금 덜 힘들게 수정하는가의 차이가 있을 뿐, 본질적으로 코드를 다 수정해야 하는 것은 마찬가지다.
개발자들의 게으름에 대한 욕심은 끝이 없다. 수많은 개발자들이 이 문제에 대해서 집요하게 고민해왔고, 여러가지 방향으로 해결책을 만들어왔다. 지금부터 원본 코드를 손대지 않고 로그 추적기를 적용할 수 있는 방법을 알아보자. 그게 바로 AOP가 될 것이다.
참고로,
템플릿 콜백 패턴은 실제 스프링 안에서 많이 사용되는 방식이다. xxxTemplate을 만나면 이번에 학습한 템플릿 콜백 패턴을 떠올려보면 어떻게 돌아가는지 쉽게 이해할 수 있을 것이다.
'Spring Advanced' 카테고리의 다른 글
Proxy/Decorator Pattern 2 (JDK 동적 프록시) (0) | 2023.12.14 |
---|---|
Proxy/Decorator Pattern (0) | 2023.12.13 |
Strategy Pattern (2) | 2023.12.12 |
Template Method Pattern (2) | 2023.12.12 |
ThreadLocal (0) | 2023.12.12 |