BackEnd/Spring

[Spring] 템플릿 콜백 패턴

샤아이인 2022. 8. 7.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. 템플릿 콜백 패턴 - 시작

지난 번 전략패턴 글에서 ContextV2 는 변하지 않는 템플릿 역할을 한다.

그리고 변하는 부분은 파라미터로 넘어온 Strategy에 해당된다. 

이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다.

 

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

 

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

실행될 callback 함수를 인자로 받은 ContextV2는 callback 함수를 호출만 한다.

@Slf4j
public class ContextV2 {

    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();

        // Callback 함수를 호출만 한다.
        strategy.call();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

Callback 함수가 호출 됬을때 실행할 로직 자체는 Strategy에 있다.

 

그럼 템플릿 콜백 페턴은 무엇일까?

 

일단 나는 디자인패턴을 공부할때 들어본 적이 없었던 이름이라서 이상하다 생각했는데, 역시는 역시였다.

템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에, 스프링 안에서만 이렇게 부른다.

그냥 전략 패턴에서 변하지 않는 템플릿과 인자로 넘기는 콜백 부분이 강조된 패턴이라 생각하면 된다.

 

전략 패턴에서 Context 가 템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다 생각하면 된다.

 

스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate 처럼 다양한 템플릿 콜백 패턴이 사용된다.

스프링에서 이름에 XxxTemplate 가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.

 

2. 템플릿 콜백 패턴 - 예제

템플릿 콜백 패턴을 구현해보자. 지난 번 글의 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 실행"));
    }
}

생각하는것처럼 정상작동 한다.

 

3. 템플릿 콜백 패턴 - 적용

이번에는 우리의 애플리케이션에 적용시켜보자!

▶ TraceCallback 인터페이스

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

 

다음으로는 템플릿 역할을 하면서, 시간을 측정해주는 코드를 만들자.

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

execute()는 메시지와 callback을 전달받아서 실행된다.

 

이제 Controller, Service, Repository를 수정해 보자!

 

▶ OrderControllerV5

@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(@RequestParam String itemId) {
        return template.execute("OrderController.request()", () -> {
            orderService.orderItem(itemId);
            return "ok";
        });
    }
}

람다를 사용하도록 코드를 작성하였다!

또한 trace를 의존관계 주입으로 받으면서 필요한 TraceTemplate 템플릿을 생성한다.

이러한 방식이 싫다면 애당초 TraceTemplate를 Bean으로 만들어서 사용해보 된다.

 

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

 

▶ 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()", () -> {
            //저장 로직
            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();
        }
    }
}

 

실행시 생각했던 정상적은 결과를 얻을 수 있다!

 

변하는 부분과 변하지 않는 부분을 분리하면서 SRP원칙을 적용시켰다.

 

하지만 지금까지 설명한 방식의 한계는 아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점이다.

클래스가 수백개이면 수백개를 더 힘들게 수정하는가 조금 덜 힘들게 수정하는가의 차이가 있을 뿐, 본질적으로 코드를 다 수정해야 하는 것은 마찬가지이다.

 

다음 글 부터는 원본 코드를 손대지 않고 로그 추적기를 적용할 수 있는 방법을 알아보자.

그러기 위해서 프록시 개념을 먼저 이해해야 한다.

댓글