1. 소개
스프링을 사용한다면 JdbcTemplate, RedisTemplate 등 `XXXTemplate`에 대한 단어를 접해봤을 것이다.
이는 모두 *템플릿 콜백 패턴*이라는 디자인 패턴을 사용한 기술들이다.
템플릿 콜백 패턴 자체가 GOF에서 소개하는 디자인 패턴은 아니지만, 스프링에서 나름대로 입맛에 맞게 재정의한 디자인 패턴이라고 보면 된다.
예시로 들었던 JdbcTemplate, RedisTemplate 처럼, 스프링의 많은 기술은 템플릿 콜백 패턴을 통해 핵심 비지니스 로직과 부가적인 기능을 분리한다.
템플릿 콜백 패턴이 어떻게 등장하게 되었는가를 보다 쉽게 이해하기 위해서 연관성이 있는 *템플릿 메서드 패턴*과 *전략 패턴*에 대해 먼저 알아보며 시작해보자.
우선 현재 예제 프로젝트의 문제상황이다.
public class OrderController {
private final OrderService orderService;
private final LogTrace trace;
@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;
}
}
}
실제 비지니스 로직은 `orderService.orderItem()`이 전부지만, *로그 추적기 기능을 추가*하라는 요구사항 때문에 배보다 배꼽이 커졌다. 로그 추적기에 관련된 코드가 너무 많아진 것이다. 우리의 목표는 이를 간소화하는 것이다.
2. 템플릿 메서드 패턴
- 소개
템플릿 메서드 패턴은 말 그대로 어떠한 템플릿을 활용해 *변하지 않는 부분에 대해서 정의*해두고 *변하는 부분은 추상 메소드*로 두어 자식에게 구현을 강제하도록 만드는 기법이다.
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
// 비지니스 로직 있다고 가정
call();
// 비지니스 로직 종료
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
log.info("resultTime={}", totalTime);
}
protected abstract void call();
}
public class SubClassLogic1 extends AbstractTemplate{
@Override
protected void call() {
log.info("비지니스 로직 1 실행");
}
}
부가 기능(= 변하지 않는 부분)에 대해서 미리 정의를 해두고, 자식이 재정의한 핵심 기능(= 비지니스 로직)을 호출하도록 하는 것이다.
public class OrderController {
private final OrderService orderService;
private final LogTrace trace;
@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()");
}
}
실제 현재 예제 애플리케이션에 적용해보면 아래와 같이 *로그 추적기에 대한 코드(= 부가기능)*가 사라진 모습을 확인할 수 있다.
물론 익명 내부 클래스를 생성하는 코드가 조금 지저분한 모습이 단점으로 남아있지만, 요청을 처리하는 컨트롤러 입장에서는 더 이상 로그 추적기를 다루는 코드가 없어진 것이 큰 장점이다.
- 단점
템플릿 메서드 패턴은 많은 부분을 해결해주었지만 *상속*을 통해 문제를 해결한다는 점이 아쉽다.
객체 지향 세계에서는 "상속보단 위임을" 이라는 말도 있듯이, 상속에서 오는 단점들이 있다.
- 자식 클래스와 부모 클래스카 컴파일 타임에 강하게 결합한다.
- 부모 클래스를 의존한다.
- 자식 입장에서는 현재 부모 클래스의 기능을 전혀 사용하지 않는데, 부모 클래스를 의존하고 알아야한다.
- 이것은 좋은 설계가 아니다.
- 상속 구조를 사용하기 때문에 별도의 클래스나 익명 내부 클래스를 통해 자식을 구현해야한다.
이러한 문제를 조금 더 깔끔하게 해결하는 방법이 아래에 설명할 *전략(strategy) 패턴*이다.
3. 전략(Strategy) 패턴
전략 패턴은 핵심 기능과 부가 기능을 분리한다는 면에서 템플릿 메서드 패턴과 컨셉 자체는 비슷하다.
전략 패턴은 변하지 않는 부분을 `Context` 라는 곳에 두고, 변하는 부분을 `Strategy`라는 인터페이스로 생성하고 해당 인터페이스를 구현하도록 만들어 문제를 해결한다.
*상속이 아니라 위임*을 통해 문제를 해결하게 된 것이다!
실제 스프링에서 의존관계 주입에 사용되는게 바로 이 전략패턴이다.
보통 컨트롤러나 서비스 단에서 하위 계층을 주입받을 때 인터페이스를 통해 의존관계를 주입받고, 구현체를 알아서 선택할 수 있게끔 만드는 패턴이 이러한 전략 패턴을 활용한 것이라고 생각하면 된다.
이러한 전략 패턴을 구현하는데는 크게 두 가지 방식이 있다.
- Strategy 인터페이스를 필드에 가지고 있는 방식
- 메소드 파라미터로 Strategy를 주입받는 방식
public class ContextV1 {
private Strategy strategy;
public ContextV1(Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
long startTime = System.currentTimeMillis();
// 비지니스 로직 있다고 가정
strategy.call(); // 위임
// 비지니스 로직 종료
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
log.info("resultTime={}", totalTime);
}
}
public class ContextV2 {
public void execute(Strategy strategy) {
long startTime = System.currentTimeMillis();
// 비지니스 로직 있다고 가정
strategy.call(); // 위임
// 비지니스 로직 종료
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
log.info("resultTime={}", totalTime);
}
}
위 두가지는 큰 차이를 갖는다.
첫 번째 방식, `ContextV1`이 스프링에서 의존관계 주입에 사용되는 방식이다.
스프링의 경우 의존관계를 주입하고 사실상 런타임에 의존관계가 변경될 일이 거의 없다.
따라서 현재처럼 *선 조립, 후 실행*하는 방식에서 매우 유용하다.
따라서 두 번째 방식이 런타임에 `Strategy`를 변경할 수 있으므로 현재 상황에서는 유리하다.
현재는 로그 추적기를 변하지 않는 것으로 두고, 각 계층에서의 비지니스 로직을 `Strategy`로 구현하는 방식이기 때문이다.
4. 템플릿 콜백 패턴
콜백이란, 다른 코드의 파라미터로 넘겨주는 실행 가능한 코드를 뜻한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수 도 있고, 아니면 나중에 실행할 수 도 있다.
쉽게 말해 현재 방금까지 알아본 전략 패턴 중 두 번째 상황에서, `Strategy`를 파라미터로 넘기기 때문에 이게 콜백 함수이다.
스프링은 이렇게 전략 패턴 중에서 파라미터로 `Strategy`를 넘기는 방식을 템플릿 콜백 패턴이라고 재정의했다.
`Context`가 템플릿의 역할을 하고 `Strategy`가 콜백으로 넘어온다고 생각하면 된다.
실제로는 `Context` -> `Template`, `Strategy` -> `Callback`으로 변한 것이 전략 패턴과의 차이점이라고 보면 된다.
전략 패턴에서 단지 템플릿과 콜백이 강조된 패턴이라고 생각하자!
실제 예제 애플리케이션에 적용해보자.
public interface TraceCallback<T> {
T call();
}
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;
}
}
}
@RestController
public class OrderController {
private final OrderService orderService;
private final TraceTemplate traceTemplate;
public OrderControllerV5(OrderService orderService, LogTrace logTrace) {
this.orderService = orderService;
this.traceTemplate = new TraceTemplate(logTrace);
}
@GetMapping("/v5/request")
public String request(String itemId) {
return traceTemplate.execute("OrderController.request()", () -> {
orderService.orderItem(itemId);
return "OK";
});
}
}
템플릿 콜백 패턴을 사용함으로써 부가 기능을 코드에서 분리할 수 있고, 상속에 대한 문제도 해결할 수 있었다. 추가로 전략 패턴에서 Strategy를 필드에 두어 의존관계를 주입받는 상황도 해결할 수 있었다.
하지만 그래도, 여전히 원본 코드 자체는 변경되고 있다. 수정 사항이 생겼을 때 많이 귀찮냐, 조금 덜 귀찮냐의 차이지 손을 봐야하는 건 매한가지이다. 이런 문제들을 해결하는 방법은 추후 포스팅에서 알아보자!
출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편
'Spring' 카테고리의 다른 글
[Spring] 동적 프록시(JDK Dynamic Proxy, CGLIB) (2) | 2024.03.05 |
---|---|
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2024.03.04 |
[Spring] 필드 동기화와 ThreadLocal (0) | 2024.03.01 |
[Spring] 스프링 트랜잭션 전파(Propagation) (0) | 2024.02.28 |
[Spring] @Transactional의 다양한 기능과 주의사항 (1) | 2024.02.27 |