앞선 포스팅에서 전략 패턴에 대해서 알아보았다.
아래의 코드는 전략 패턴에서 봤던 Context이다.
@Slf4j
public class ContextV2 {
// strategy 주입 로직 생략
public void execute(Strategy strategy) {
long startTime = System.currentTimeMillis();
// Strategy에 비즈니스 로직을 위임
strategy.call();
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("result={}",resultTime);
}
}
여기서 ContextV2는 변하지 않는 템플릿 역할을 하고, 변하는 부분은 Strategy의 코드를 실행해서 처리한다.
이처럼 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라고 한다.
콜백 정의
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)는 다른 코드의 인수로서
넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도
있고, 아니면 나중에 실행할 수도 있다. (위키백과 참고)
callback은 코드가 호출(call)은 되는데, 코드를 넘겨준 쪽의 뒤(back)에서 실행된다는 뜻이다.
자바에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다.
자바 8이전에는 인터페이스를 구현하고, 주로 익명 내부 클래스를 활용해 콜백을 구현했다.
자바 8이후로 최근에는 주로 람다를 사용한다.
스프링에서는 처음에 확인한 ContextV2 같은 방식을 템플릿 콜백 패턴이라고 한다.
Context가 템플릿 역할을 하고, Strategy가 콜백으로 넘어오는 것이다.
즉, 템플릿을 실행하는 시점에 콜백을 넘기는 방식을 말한다.
스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate등등 다양한 템플릿 콜백 패턴이 사용된다. 스프링에서 이름에 XXXTemplate이 있다면 템플릿 콜백 패턴으로 만들어졌다고 생각하면 된다.
그럼 앞서 탬플릿 메서드 패턴을 활용해 변경했던 로그 추적기에 템플릿 콜백 패턴을 적용해보자.
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;
}
}
}
변하지 않는 부분, 템플릿을 생성하자.
여기서 템플릿의 파라미터로 변하는 부분, 콜백(TraceCallback, 핵심 로직)을 받는다는 것을 기억하자.
여기서 LogTrace는 인터페이스이며 구현체는 ThreadLocal을 활용하고 있다.
public interface TraceCallback<T> {
T call();
}
@RestController
public class OrderControllerV5 {
private final OrderServiceV5 orderService;
private final TraceTemplate template;
public OrderControllerV5(OrderServiceV5 orderService, LogTrace logTrace) {
this.orderService = orderService;
this.template = new TraceTemplate(logTrace);
}
@GetMapping("/v5/request")
public String request(String itemId) {
return template.execute("OrderController.request()", () -> {
orderService.orderItem(itemId);
return "ok";
});
}
}
생성자에서 파라미터로 LogTrace를 주입받아 template을 생성한다.
이제 클라이언트는 템플릿을 실행하는데, 이 실행 시점에 콜백(핵심 로직)을 파라미터로 넘긴다.
만약 익명 내부 클래스로 넘긴다고 한다면 아래처럼 넘길 수 있다.
return template.execute("OrderController.request()", new TraceCallback<String>() {
@Override
public String call() {
orderService.orderItem(itemId);
return "ok";
}
});
@Service
public class OrderServiceV5 {
private final OrderRepositoryV5 orderRepository;
private final TraceTemplate template;
public OrderServiceV5(OrderRepositoryV5 orderRepository, LogTrace logTrace) {
this.orderRepository = orderRepository;
this.template = new TraceTemplate(logTrace);
}
public void orderItem(String itemId) {
template.execute("OrderService.orderItem()", () -> {
orderRepository.save(itemId);
return null;
});
}
}
@Repository
public class OrderRepositoryV5 {
private final TraceTemplate template;
public OrderRepositoryV5(LogTrace logTrace) {
this.template = new TraceTemplate(logTrace);
}
// 저장 로직
public void save(String itemId) {
template.execute("OrderRepository.save()", () ->{
if (itemId.equals("ex")){
throw new IllegalStateException("예외 발생!");
}
sleep(1000);
return null;
});
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
로그 추적기를 이용해 메서드의 병목현상을 추적하는 부가기능을 구현하였고, 이를 어플리케이션에 적용시켰다.
그 과정에서 어플리케이션의 로직은 적용 방법에 따라서 변화하였는데, 변경사항을 한번 정리하고 가자,
// 1. 핵심 로직만 존재
@GetMapping("/v0/request")
public String request(String itemId) {
orderService.orderItem(itemId);
return "ok";
}
// 2. 로그 추적 기능 추가
@GetMapping("/v3/request")
public String request(String itemId) {
TraceStatus status = null;
try {
status = trace.begin("OrderController.request()");
orderService.orderItem(itemId);
trace.end(status);
return "ok";
} catch (Exception e) {
trace.exception(status, e);
//예외를 꼭 다시 던져줘야 한다.
//로그는 어플리케이션 흐름에 영향을 줘선 안된다.
throw e;
}
}
// 3. 템플릿 메서드 패턴 적용
@GetMapping("/v4/request")
public String request(String itemId) {
AbstractTemplate<String> template = new AbstractTemplate<>(trace) {
@Override
protected String call() {
orderService.orderItem(itemId);
return "ok";
}
};
return template.execute("OrderController.request()");
}
// 4. 템플릿 콜백 패턴 적용
@GetMapping("/v5/request")
public String request(String itemId) {
return template.execute("OrderController.request()", new TraceCallback<String>() {
@Override
public String call() {
orderService.orderItem(itemId);
return "ok";
}
});
}
하지만 결국 아무리 많은 코드를 줄여도 원본 코드를 수정해야 하는 것에는 변함이 없다.
부가 기능을 사용하는 클래스를 얼마나 더 많이 수정하냐 덜 수정하냐의 차이만 있을 뿐이다.
이에 대한 해결책으로 제시된 것이 프록시이다. 다음 포스팅에서는 프록시에 대해 알아보자.
출처 : 김영한 - 스프링 핵심 원리 고급편