ContextV2
는 변하지 않는 템플릿 역할을 한다.
그리고 변하는 부분은 파라미터로 넘어온 Strategy
의 코드를 실행해 처리한다.
이렇게 다른 코드의 인수로써 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다.
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)는 다른 코드의 인수로써 넘겨주는 실행 가능한 코드를 말한다.
콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
(위키백과 참고)
쉽게 말해 callback
은 코드가 호출(call
)은 되는데 코드를 넘겨준 곳 뒤(back
)에서 실행된다는 뜻이다.
ContextV2
예제에서 콜백은 Strategy
이다.Strategy
를 실행하는 것이 아니라, 클라이언트가 ContextV2.execute(..)
를 실행할 때 Strategy
를 넘겨주고, ContextV2
뒤에서 Strategy
가 실행된다.ContextV2
와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라고 한다.Context
가 템플릿 역할을 하고, Strategy
부분이 콜백으로 넘어온다고 생각하면 된다.JdbcTemplate
, RestTemplate
, TransactionTemplate
, RedisTemplate
처럼 다양한 템플릿 콜백 패턴이 사용된다.XxxTemplate
가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.: 템플릿 콜백 패턴 구현하기
ContextV2
와 내용은 같고 이름만 다르다.
Context
→Template
Strategy
→Callback
public interface Callback {
void call();
}
➡️ 콜백 로직을 전달할 인터페이스
@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);
}
}
@Slf4j
public class TemplateCallbackTest {
/**
* 템플릿 콜백 패턴 - 익명 내부 클래스
*/
@Test
void callbackV1() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(new Callback() { // new Callback()을 넘기는 것 (익명 내부 클래스)
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
});
template.execute(new Callback() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
});
}
/**
* 템플릿 콜백 패턴 - 람다
*/
@Test
void callbackV2() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(() -> log.info("비즈니스 로직1 실행")); // () -> log.info("비즈니스 로직1 실행") -> 이 부분이 템플릿 뒤쪽에서 실행됨 (callback)
template.execute(() -> log.info("비즈니스 로직2 실행"));
}
}
별도의 클래스를 만들어 전달해도 되지만, 콜백을 사용할 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다.
물론, 여러 곳에서 함께 사용되는 경우 재사용을 위해 콜백을 별도의 클래스로 만들어도 된다.
: 템플릿 콜백 패턴 애플리케이션에 적용하기
public interface TraceCallback<T> { // 제네릭 사용
T call();
}
<T>
제네릭 사용하였고 콜백의 반환 타입을 정의한다.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; // (먹었으니 정상 흐름이 되어버림 그래서 )예외를 꼭 다시 던져주어야 한다.
}
}
}
TraceTemplate
는 템플릿 역할execute(..)
를 보면 message
데이터와 콜백인 TraceCallback callback
을 전달 받는다.<T>
(📌) 제네릭을 사용하였고 반환 타입을 정의한다.⚠️ v5에 복사하고
v5
로 링크와 이름을 수정한 뒤 한 번 실행시켜보아야 한다. (쌓아두면 나중에 큰 일이 될 수 있음)
@RestController // @Controller + @ResponseBody
public class OrderControllerV5 {
private final OrderServiceV5 orderService;
private final TraceTemplate template;
public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
this.orderService = orderService;
this.template = new TraceTemplate(trace);
}
@GetMapping("/v5/request")
public String request(String itemId) {
return template.execute("OrderController.request()", new TraceCallback<>() {
@Override
public String call() {
orderService.orderItem(itemId);
return "ok";
}
});
}
}
private final TraceTemplate template;
: 계속 생성하는 것이 번거로우므로 이렇게 한 번 생성해두면 어차피 OrderControllerV5
가 싱글톤이므로 생성자는 한 번 호출되므로 편리하다.
this.template = new TraceTemplate(trace)
: trace
의존 관계를 주입 받으면서 필요한 TraceTemplate
템플릿을 생성한다.
(📌 참고로 TraceTemplate
를 처음부터 스프링 빈으로 등록하고 주입 받아도 된다. 이건 알아서 선택해 사용할 것)
template.execute(.., new TraceCallback(){..})
: 템플릿을 실행하면서 콜백을 전달한다.
여기서는 콜백으로 익명 내부 클래스를 사용했다.
@Service // @Service 안에 @Component가 있어 자동으로 컴포넌트 스캔의 대상이 됨(자동으로 스프링 빈 등록)
public class OrderServiceV5 {
private final OrderRepositoryV5 orderRepository;
private final TraceTemplate template;
public OrderServiceV5(OrderRepositoryV5 orderRepository, LogTrace trace) {
this.orderRepository = orderRepository;
this.template = new TraceTemplate(trace);
}
public void orderItem(String itemId) {
template.execute("OrderService.orderItem()", () -> {
orderRepository.save(itemId);
return null;
});
}
}
template.execute(.., new TraceCallback(){..})
@Repository // 안에 @Component가 있어 자동으로 컴포넌트 스캔 대상이 됨(스프링 빈 등록)
public class OrderRepositoryV5 {
private final TraceTemplate template;
public OrderRepositoryV5(LogTrace trace) {
this.template = new TraceTemplate(trace);
}
public void save(String itemId) {
template.execute("OrderRepository.save()", () -> {
// 저장 로직
if (itemId.equals("ex")) { // (다양한 예제를 위해)"ex"라는 것이 넘어오면 예외 발생시킬
throw new IllegalStateException("예외 발생!");
}
sleep(1000); // 상품을 저장하는데 1초 정도 걸린다고 가정
return null;
});
}
...
➡️ 나머지 부분은 V4와 동일
(Template Callback Pattern)
: 전략 패턴의 변형으로 DI(Dependency Injection) 의존성 주입에서 사용하는 특별한 전략 패턴
즉, 템플릿 콜백 패턴은 (📌)전략 패턴에 익명 내부 클래스를 가미해 사용하는 방법이다.
📌 전략 패턴
(Strategy Pattern)
전략 패턴을 구성하는 3가지 요소
- 전략 메소드를 가진 전략 객체
- 전략 객체를 사용하는 컨텍스트(전략 객체의 사용자/소비자)
- 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제 3자, 전략 객체의 공급자)
클라이언트는 다양한 전략 중 현재 상황에 적합한 전략을 생성해 컨텍스트에게 전략 객체를 주입한다.
즉, 다양하게 전략을 변경하며 컨텍스트에게 주입하는 것
➡️ 클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에게 주입하는 패턴
(Generic)
: 데이터 타입을 일반화하다.
즉, 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것
특정 타입을 미리 지정하지 않고, 필요에 의해 지정할 수 있도록 하는 일반타입을 말한다.
정확히 말하자면 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다.
이것이 암묵적인 규칙이다. <V>
를 <Val>
로 해도 되지만 통상적으로 이렇게 많이 쓰기 때문에 대단한 경우가 아니면 표의 내용처럼 사용하자.