스프링 핵심 원리(고급편)

Ch03. 템플릿 메서드 패턴과 콜백 패턴 - 템플릿 콜백 패턴

webmaster 2022. 4. 10. 00:57
728x90

ContextV2는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온 Strategy의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다.

콜백

  •  프로그래밍에서 콜백(callback) 또는 콜 애프터 함수(call-after function)는 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
  • callback 은 코드가 호출( call )은 되는데 코드를 넘겨준 곳의 뒤( back )에서 실행된다는 뜻이다.
    • ContextV2 예제에서 콜백은 Strategy 이다.
    • 여기에서는 클라이언트에서 직접 Strategy를 실행하는 것이 아니라, 클라이언트가 ContextV2.execute(..)를 실행할 때 Strategy를 넘겨주고, ContextV2 뒤에서 Strategy 가 실행된다.
  • 템플릿 콜백 패턴
    • 스프링에서는 ContextV2와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라 한다. 전략 패턴에서 Context 가 템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다 생각하면 된다.
    • 참고로 템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에, 스프링 안에서만 이렇게 부른다. 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라 생각하면 된다.
    • 스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate처럼 다양한 템플릿 콜백 패턴이 사용된다. 스프링에서 이름에 XxxTemplate 가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다

템플릿 콜백 패턴 구성도

테스트 예제

Callback 

public interface Callback {
    void call();
}

Template

@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);
    }
}

Test

템플릿 콜백 패턴 - 익명 내부 클래스

/**
 * 템플릿 콜백 패턴 - 익명 내부 클래스
 */
@Test
public void callbackV1(){
    TimeLogTemplate template = new TimeLogTemplate();
    template.execute(new Callback() {
        @Override
        public void call() {
            log.info("비지니스 로직1 실행");
        }
    });
    template.execute(new Callback() {
        @Override
        public void call() {
            log.info("비지니스 로직2 실행");
        }
    });
}

템플릿 콜백 패턴 - 람다

/**
 * 템플릿 콜백 패턴 - 람다
 */
@Test
public void callbackV2(){
    TimeLogTemplate template = new TimeLogTemplate();
    template.execute(() -> log.info("비지니스 로직1 실행"));
    template.execute(() -> log.info("비지니스 로직2 실행"));
}

적용

변경되는 부분(CallBack)

public interface TraceCallback<T> {
    T call();
}

변경되지 않는 부분(Template)

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;
        }
    }
}

Controller

@RestController
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<String>() {
            @Override
            public String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        });
    }
}

Service

@Service
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()", (TraceCallback<Void>) () -> {
            orderRepository.save(itemId);
            return null;
        });
    }
}

Repository

@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()", (TraceCallback<Void>) () -> {
            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();
        }
    }
}

한계 

아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점이다. 클래스가 수백 개이면 수백 개를 더 힘들게 수정하는가 조금 덜 힘들게 수정하는가의 차이가 있을 뿐, 본질적으로 코드를 다 수정해야 하는 것은 마찬가지이다.

프록시 개념을 이용하면 이를 해결할 수 있다.

728x90