이전에 템플릿 메서드 패턴 포스팅에서 JdbcTemplate가 템플릿 메서드 패턴을 개선한 전략 패턴과 이를 이용한 스프링의 템플릿 콜백 패턴을 통하여 구현이 된다고 포스팅을 했었는데, 오늘은 그 템플릿 콜백 패턴이 어떻게 구성되는지 알아보는 시간입니다.
JdbcTemplate 말고도, TransactionTemplate, RedisTemplate 등등 많은 부분에서 활용되기 때문에 해당 패턴을 이해하고 나면 이제 그림이 그려지실거라 생각합니다.
그럼 들어가기에 앞서서 먼저 콜백에 대해서 알 필요가 있습니다.
콜백이란?
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
해당 정의는 위키백과를 참조한 정의입니다.
하지만 정의를 봐도 무슨 말은 하는지 감이 안 잡힐 수도 있을 것 같습니다. 무슨 말을 하는건지.. 이걸 쉽게 이해한다면 다음과 같이 설명 할 수 있을 것 같습니다.
A라는 메서드를 실행하는데 A라는 메서드 매개변수에 어떤 하나의 객체를 보내고 그 객체가 A 메서드 내부에서 실행하도록 하는 것. 이렇게 얘기하니 뭔가 낯이 익을거라고 생각합니다. 바로 우리가 전략패턴에서 사용했던, 알고리즘 군을 따로 나누고 알고리즘 군을 파라미터로 받아서 사용하는 전략 패턴과 동일한 방식입니다.
즉, 템플릿 콜백 패턴은 그저 전략 패턴과 거의 동일한데, 스프링에서 해당 패턴으로 부르고 있는 것입니다. 따라서 지금까지 배웠던 다른 패턴들처럼 GOF에 해당하는 디자인 패턴이 아닙니다.
템플릿 콜백 패턴의 구조
이전 시간에 배웠던 전략패턴과 크게 다르지 않은 모습을 볼 수 있습니다. 구조도 보고, 그럼 해당 구조가 어떻게 구현이 되어 있는지 확인을 해보도록 합시다.
public interface Callback {
void call();
}
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);
}
}
저희가 이전에 배웠던 전략패턴과 똑같은 모습을 볼 수 있지요.
따라서, 사용법도 이전에 전략패턴을 사용했던 것과 똑같은 것을 또 볼 수 있어요.
@Test
void callbackV2() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(() -> log.info("비즈니스 로직1 실행"));
template.execute(() -> log.info("비즈니스 로직2 실행"));
}
이제 우리는 어떠한 템플릿을 만들고, 그 알고리즘만 따로 구현하도록 해서, 행동의 실행을 인터페이스에 의존해서 다형성을 통해 구현하도록 할 수 있게 되었습니다.
템플릿 콜백 패턴의 trade-off
로그 추적기를 만들기 위해 다음과 같은 템플릿을 만들었습니다.
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;
}
}
}
이제 이를 스프링에서 사용하기 위해서는 2가지 방법이 있을 것 입니다.
1. TraceTemplate를 빈으로 직접 등록해서 스프링 컨테이너에서 관리되도록 하는 방법
2. 컴파일 때, 필요한 곳에서 객체를 생성해서 사용하는 방법
위의 제목처럼 이 두 가지 방식에서는 Trade-Off 를 알고 사용해야 하는데요. 저는 당연히 1번이 가장 편할거라 생각해서 1번 방식을 택했었습니다. 다음과 같이 말이죠.
직접 Bean으로 등록하기
@Configuration
public class LogTraceConfig {
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
@Bean
public TraceTemplate traceTemplate() {
return new TraceTemplate(logTrace());
}
}
이렇게 되면 앞으로 Template를 사용하려고 하는 곳에서 Lombok의 @RequiedArgsConstructor 를 사용해서 간단하게 의존주입받고 사용 할 수 있습니다.
하지만 이 때 발생하는 문제점이 있습니다. 애플리케이션을 사용하는 부분에 있어서는 크게 문제가 없지만 테스트 코드를 작성할 때 문제가 발생합니다.
테스트 코드에서는 메인 패키지에서 만든 싱글톤 컨테이너를 인식할 수 없습니다.
Test 코드에서도 따로 @TestConfiguration을 통해서 또 동일하게 빈을 생성할 수 있도록 해주어야 합니다.
테스트 클래스가 한 개라면 상관없겠지만, 로그 추적기는 여러 클래스에서 사용이 될테니, 여러 클래스에 영향을 미치게 됩니다.
애플리케이션을 구현하는데에는 굉장히 편리해지지만, 테스트 코드를 작성하는데에 있어서 더 많은 코드를 작성하게 된다는 문제점이 발생하고 만 것입니다.
따라서 이를 해결하려면 어떻게 해야할까요?
수동으로 객체 주입하기
private final OrderServiceV5 orderService;
private final TraceTemplate traceTemplate;
public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
this.orderService = orderService;
this.traceTemplate = new TraceTemplate(trace);
}
바로 필요한 영역에서 다음과 같이 주입이 되도록 해주는 것입니다.
그럼 실제로 다른 곳에서 컨트롤러를 테스트 코드를 활용하려면 다음과 같이 하면 될 것입니다.
LogTrace trace = new ThreadLocalLogTrace();
OrderRepository repository = new OrderRepository(trace);
OrderService service = new OrderService(repository, trace);
OrderController controller = new OrderController(service, trace);
다음과 같이 이제 사용하고 싶은 곳에 적재적소에 맞게 선언해주어, 이제 불필요하게 @TestConfiguration을 계속 만들지 않아도 되는 상황을 만들 수가 있습니다.
그러나, 여기에도 단점이 존재합니다. 실제로 애플리케이션이 컴파일될 때 인터페이스의 구상체를 보고 직접 new 키워드를 통해 객체를 생성하는 것이니 하나의 객체만 존재하는 싱글톤과 달리 더 많은 객체가 생성 될 수 있다는 것인데요.
하지만, 만약 클라이언트가 호출할 때마다 객체가 생성되어야 한다면, 그 부분은 많은 메모리가 손실될테지만, 해당 객체는 애플리케이션 실행 시 한 번만 생성되는 객체라서 생각보다 큰 메모리를 잡아먹지 않는다는 것입니다.
따라서, 실제로 애플리케이션을 개발 할 때에는 테스트에 더 신경을 써주어야 하기 때문에 후자의 방식을 택하는 것이 좀 더 좋지 않을까 라고 생각합니다만, 역시 개인의 선택이지요. 둘의 차이가 무엇인지 정확하게 파악하고 사용하는 것이 중요할 것 같습니다.
템플릿 콜백 패턴의 문제
하지만 이 패턴 또한 문제가 있지요. 해당 템플릿을 사용하려는 코드에 계속 템플릿을 사용할 수 있도록 구성을 활용해야 한다는 점입니다. 위에서 만든 로그 추적기는 결국 한 프로젝트에 모든 클래스에 들어가야 할텐데, 그럼 너무 번거롭지 않을까요?
이를 해결하기 위해 등장한 디자인 패턴이 프록시 패턴입니다. 어디선가 들어본 것 같지 않나요? 사실 저희가 빈을 등록 할 때에도 실제 객체가 등록되는 것이 아닌, Proxy 패턴을 통해서 스프링 컨테이너에 등록이 된다. 라는 말을 어디선가 들어봤을 수도 있을 것 같아요.
(스프링부트에 한해서 입니다. 레거시 스프링 프로젝트에서 xml로 등록하면 직접 해당 객체가 등록됩니다.)
다음에는 기회가 되면 프록시 패턴을 한 번 포스팅 해보도록 하겠습니다. 오늘의 포스팅은 여기까지 입니다.
'Backend > Spring' 카테고리의 다른 글
자바 리플렉션과 이를 이용한 JDK 동적 프록시와 CGLIB (0) | 2023.04.20 |
---|---|
프록시 (0) | 2023.04.18 |
스프링의 싱글톤 패턴으로 인한 동시성 문제와 그 해결 (0) | 2023.04.14 |
1. SPRING (0) | 2022.06.13 |
sts4 응용프로그램을 열 수 없습니다. (1) | 2022.06.07 |