FireDrago
전략패턴과 콜백패턴의 적용 본문
전략패턴
이전에 템플릿 메서드 패턴에 대해서 알아보았다.
템플릿 메서드 패턴은 핵심기능과 부가기능을 상속을 사용하여 분리했는데,
자식 클래스가 부모클래스의 코드를 사용하지 않아도 강하게 결합되는 문제와
부모클래스의 변경이 자식클래스에도 영향을 미칠 수 있다는 단점이 있었다.
이를 해결하기 위해서 전략패턴에 대해 알아보자
전략 패턴은 부가기능을 Context 라는 곳에 두고, 핵심기능을 Strategy 라는 인터페이스를 만들고 해당
인터페이스를 구현하도록 해서 문제를 해결한다. 상속이 아니라 위임으로 문제를 해결하는 것이다.
public interface Strategy {
void call();
}
// Strategy를 구현하는 각각의 핵심 기능들
@Slf4j
public class StrategyLogic1 implements Strategy{
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
@Slf4j
public class StrategyLogic2 implements Strategy{
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
}
이렇게 핵심기능은 인터페이스를 구현체로 두고 Context 에 부가기능을 둔뒤, 핵심기능을 호출한다.
핵심기능을 호출하는 두가지 방법이 있는데, 필드에 전략을 보관하는 방식과, 파라미터로 전달받는 방식이 있다.
1. Strategy를 필드에 보관하는 방식

/**
* 필드에 전략을 보관하는 방식
*/
@Slf4j
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 resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
2. Strategy를 파라미터로 전달받는 방식

/**
* 전략을 파라미터로 전달받는 방식
*/
@Slf4j
public class ContextV2 {
public void execute(Strategy strategy) {
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
strategy.call();
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
1번 방식은 Context 와 Strategy 를 실행 전에 원하는 모양으로 조립해두고,
그 다음에 Context 를 실행하는 선 조립, 후 실행 방식에서 매우 유용하다.
Context 와 Strategy 를 한번 조립하고 나면 이후로는 Context 를 실행하기만 하면 된다.
스프링으로 애플리케이션을 개발할 때 애플리케이션 로딩 시점에 의존관계 주입을 통해
필요한 의존관계를 모두 맺어두고 실제 요청을 처리하는 것 과 같은 원리이다.
이 방식의 단점은 Context 와 Strategy 를 조립한 이후에는 전략을 변경하기가 번거롭다는 점이다.
Context 에 setter 를 제공해서 Strategy 를 넘겨 받아 변경하면 되지만,
Context 를 싱글톤으로 사용할 때는 동시성 이슈 등 고려할 점이 많다.
반면 2번 방식은 애플리케이션 의존 관계를 설정하는 것 처럼 선 조립, 후 실행이 아니다.
단순히 코드를 실행할 때 변하지 않는 템플릿이 있고, 그 템플릿 안에서 원하는 부분만 살짝 다른 코드를 실행한다.
따라서 2번 방식은 실행 시점에 유연하게 실행 코드 조각을 전달한다.
템플릿 콜백 패턴
위 코드에서 ContextV2 클래스는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온 Strategy 의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다
스프링에서는 ContextV2 와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라 한다. 전략 패턴에서 Context 가
템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다 생각하면 된다.
스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate 처럼 다양
한 템플릿 콜백 패턴이 사용된다. 스프링에서 이름에 XxxTemplate 가 있다면 템플릿 콜백 패턴으로 만들어져
있다 생각하면 된다.

템플릿 콜백 패턴 로그 추적기에 적용하기
앞선 포스트에서 로그 추적기를 만들고, 템플릿 메서드 패턴을 적용 해봤다.
이제는 템플릿 콜백 패턴을 적용하여 로그 추적기를 사용해보자
// 제네릭스를 통해 클라이언트가 반환타입을 결정할 수 있다.
// 타입 유연성이 높아졌다.
public interface TraceCallback<T> {
T call();
}
우선 TraceCallback 인터페이스를 만들고, 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 클래스를 만들었다.
TraceCallback 인터페이스를 파라미터로 전달받아 내부에서 TraceCallback.call() 메서드를 호출하여,
핵심 비즈니스 로직을 호출한다. TraceCallback에서 제네릭스를 사용했으므로, execute() 역시 제네릭스를 사용했다.
@RestController
public class OrderControllerV5 {
private final OrderServiceV5 orderService;
private final TraceTemplate template;
public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
this.orderService = orderService;
template = new TraceTemplate(trace);
}
@GetMapping("/v5/request")
public String request(@RequestParam("itemId") String itemId) {
return template.execute("OrderController.request()", new TraceCallback<String>() {
@Override
public String call() {
orderService.orderItem(itemId);
return "ok";
}
});
}
}
컨트롤러는 생성자를 통해 LogTrace를 주입받는다. 주입받은 LogTrace 를 이용하여 TraceTemplate 을 생성한다.
처음부터 스프링빈으로 등록하고 주입 받아도 된다. 하지만 이 방식이 테스트하기에 좀더 편리하다.
익명클래스를 사용하여 콜백을 전달한다.
@Service
public class OrderServiceV5 {
private final OrderRepositoryV5 orderRepository;
private final TraceTemplate template;
public OrderServiceV5(OrderRepositoryV5 orderRepository, LogTrace trace) {
this.orderRepository = orderRepository;
template = new TraceTemplate(trace);
}
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 trace) {
this.template = new TraceTemplate(trace);
}
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) {
e.printStackTrace();
}
}
}
정리
콜백 패턴과 람다등을 활용하여 로그 추적 기능을 편리하게 사용할 수 있게 되었다.
하지만 여전히 원본 코드에 로그 추적을 위한 코드를 수정하여 넣어줘야 한다.
이를 해결하기위해 개발자들은 프록시 패턴을 사용하게 된다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 객체를 생성하는 방법 (생성자, 정적메서드, 빌더패턴) (0) | 2024.06.05 |
|---|---|
| 프록시 적용하기 (로그 추적기) (0) | 2024.05.13 |
| 프록시 패턴과 데코레이터 패턴 (0) | 2024.05.13 |
| 템플릿 메서드 패턴과 적용 (0) | 2024.05.10 |
| [java] 싱글톤 패턴 (0) | 2023.05.04 |
