본문 바로가기

카테고리 없음

[디자인 패턴] 템플릿 콜백 패턴

목차

  • 템플릿 콜백 패턴 - 시작
  • 템플릿 콜백 패턴 - 예제
  • 템플릿 콜백 패턴 - 적용

 

템플릿 메서드 패턴 - 시작

ContextV2 는 변하지 않는 템플릿 역할을 한다. 

/**
 * 전략을 파라미터로 전달 받는 방식
 */
@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);
    }
}
  • 그리고 변하는 부분은 파라미터로 넘어온 Strategy 의 코드를 실행해서 처리한다. 
  • 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다

 

콜백 정의
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다. 콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다. 

 

 

쉽게 이야기해서 callback 은 코드가 호출( call )은 되는데 코드를 넘겨준 곳의 뒤( back )에서 실행된다는 뜻이다.

  • ContextV2 예제에서 콜백은 Strategy 이다.
  • 여기에서는 클라이언트에서 직접 Strategy 를 실행하는 것이 아니라, 클라이언트가 ContextV2.execute(..) 를 실행할 때
  • Strategy 를 넘겨주고, ContextV2 뒤에서 Strategy 가 실행된다.


자바 언어에서 콜백 

  • 자바 언어에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다. 
  • 자바8부터는 람다를 사용할 수 있다.
  • 자바 8 이전에는 보통 하나의 메소드를 가진 인터페이스를 구현하고, 주로 익명 내부 클래스를 사용했다.
  • 최근에는 주로 람다를 사용한다.


템플릿 콜백 패턴

  • 스프링에서는 ContextV2 와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라 한다.
  • 전략 패턴에서 Context 가 템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다 생각하면 된다.
  • 참고로 템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에, 스프링 안에서만 이렇게 부른다. 
  • 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라 생각하면 된다.
  • 스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate 처럼 다양한 템플릿 콜백 패턴이 사용된다. 
  • 스프링에서 이름에 XxxTemplate 가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.

 

 


템플릿 콜백 패턴 - 예제

템플릿 콜백 패턴을 구현해보자. ContextV2 와 내용이 같고 이름만 다르므로 크게 어려움은 없을 것이다.

  • Context ➔ Template
  • Strategy ➔ Callback

 

Callback - 인터페이스

public interface Callback {
    void call();
}
  • 콜백 로직을 전달할 인터페이스이다.

 

TimeLogTemplate

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

 

 

TemplateCallbackTest

@Slf4j
public class TemplateCallbackTest {

    /**
     * 템플릿 콜백 패턴 - 익명 내부 클래스
     */
    @Test
    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
    void callbackV2() {
        TimeLogTemplate template = new TimeLogTemplate();
        template.execute(() -> log.info("비즈니스 로직1 실행"));
        template.execute(() -> log.info("비즈니스 로직2 실행"));
    }
}
  • 별도의 클래스를 만들어서 전달해도 되지만, 콜백을 사용할 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다.
  • 물론 여러곳에서 함께 사용되는 경우 재사용을 위해 콜백을 별도의 클래스로 만들어도 된다.

템플릿 콜백 패턴 - 적용

이제 템플릿 콜백 패턴을 애플리케이션에 적용해보자.

 

TraceCallback 인터페이스

public interface TraceCallback<T> {
    T call();
}
  • 콜백을 전달하는 인터페이스이다.
  • <T> 제네릭을 사용했다. 콜백의 반환 타입을 정의한다.

 

TraceTemplate

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> 제네릭을 사용했다. 반환 타입을 정의한다.

 

OrderControllerV5

@RestController
public class OrderControllerV5 {

    private final OrderServiceV5 orderService;
    private final TraceTemplate template;

    @Autowired // 생략 가능
    public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {
        this.orderService = orderService;
        this.template = new TraceTemplate(trace);
    }

    @GetMapping("/v5/request")
    public String request(String itemId) {

        String execute = template.execute("OrderController.request()",
                new TraceCallback<String>() {

            @Override
            public String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        });
        return execute;
    }
}
  • this.template = new TraceTemplate(trace) : trace 의존관계 주입을 받으면서 필요한 TraceTemplate 템플릿을 생성한다.
  • 참고로 TraceTemplate 를 처음부터 스프링 빈으로 등록하고 주입받아도 된다. 이 부분은 선택이다.
  • template.execute(.., new TraceCallback(){..}) : 템플릿을 실행하면서 콜백을 전달한다. 
  • 여기서는 콜백으로 익명 내부 클래스를 사용했다.

 

OrderServiceV5

@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;
        });
    }
}
  • template.execute(.., new TraceCallback(){..}) : 템플릿을 실행하면서 콜백을 전달한다. 
  • 여기서는 콜백으로 람다를 전달했다.

 

OrderRepositoryV5

@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("예외 발생!");
                    }

                    // 상품을 저장하는데 1초 정도 걸린다.
                    sleep(1000);

                    return null;
                });
    }

    private void sleep(int millis) {

        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

 

정상 실행

  • http://localhost:8080/v5/request?itemId=hello

 

정상 실행 로그

[aaaaaaaa] OrderController.request()
[aaaaaaaa] |-->OrderService.orderItem()
[aaaaaaaa] | |-->OrderRepository.save()
[aaaaaaaa] | |<--OrderRepository.save() time=1001ms
[aaaaaaaa] |<--OrderService.orderItem() time=1003ms
[aaaaaaaa] OrderController.request() time=1004ms

 

한계
그런데 지금까지 설명한 방식의 한계는 아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점이다. 클래스가 수백개이면 수백개를 더 힘들게 수정하는가 조금 덜 힘들게 수정하는가의 차이가 있을 뿐, 본질적으로 코드를 다 수정해야 하는 것은 마찬가지이다. 원본 코드를 손대지 않고 로그 추적기를 적용할 수 있는 방법을 알아보자. 그러기 위해서 프록시 개념을 먼저 이해해야 한다.