5 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

템플릿 콜백 패턴 - 시작

ContextV2는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온 Strategy의 코드를 실행해서 처리한다.

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

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

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

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

자바 언어에서 콜백

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

템플릿 콜백 패턴

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

템플릿 콜백 패턴 - 예제

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

  • ContextTemplate
  • StrategyCallback

해당 위치에 만들겠다.

hello.advanced.trace.strategy.code.template.Callback

1
2
3
public interface Callback {  
    void call();  
}

hello.advanced.trace.strategy.code.template.TimeLogTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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);  
    }  
}

hello.advanced.trace.strategy.TemplateCallbackTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@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 실행"));  
    }  
}

다음과 같이 메서드에 매개변수로 코드 조각을 넘겨서 실행 하는 것.

저번에 했던 내용과 거의 비슷해서 어렵진 않다.

템플릿 콜백 패턴 - 적용

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

해당 위치에서 작업

hello.advanced.trace.callback.TraceCallBack

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

hello.advanced.trace.callback.TraceTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TraceTemplate {  
    private final LogTrace trace;  
  
    public TraceTemplate(LogTrace logTrace) {  
        this.trace = logTrace;  
    }  
  
    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>제네릭을 사용했다. 반환 타입을 정의한다.

v4 → v5 복사

내부의 의존 관계나 파일 명 등 V4 → V5로 잘 바꿔주자.

컨트롤러

OrderControllerV5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController  
public class OrderControllerV5 {  
  
    private final OrderServiceV5 orderService;  
    private final TraceTemplate traceTemplate;  
  
    public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {  
        this.orderService = orderService;  
        this.traceTemplate = new TraceTemplate(trace);  
    }  
  
    @GetMapping("/v5/request")  
    public String request(@RequestParam("itemId") String itemId) {  
  
        return traceTemplate.execute("OrderController.request()", new TraceCallBack<>() {  
            @Override  
            public String call() {  
                orderService.orderItem(itemId);  
                return "ok";  
            }  
        });  
    }  
}

OrderControllerV4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final LogTrace trace;

@GetMapping("/v4/request")  
public String request(@RequestParam("itemId") String itemId) {  
  
    AbstractTemplate<String> template = new AbstractTemplate<>(trace) {  
        @Override  
        protected String call() {  
            orderService.orderItem(itemId);  
            return "ok";  
        }  
    };  
    return template.execute("OrderControllerV4.request()");  
}

이게 V4 코드였는데 이젠 템플릿 메서드가 아니라 템플릿 콜백 패턴을 사용하자.

LogTrace에서 traceTemplate으로 바뀌었는데, traceTemplate

안에서 LogTrace를 생성자 주입으로 받게 했기 때문에

1
2
3
4
5
6
7
private final OrderServiceV5 orderService;  
private final TraceTemplate traceTemplate;  

public OrderControllerV5(OrderServiceV5 orderService, LogTrace trace) {  
	this.orderService = orderService;  
	this.traceTemplate = new TraceTemplate(trace);  
}

다음과 같이 TraceTemplate에서 컨트롤러 생성자 호출 때 LogTrace를 넘겨 줬다.

그리고 실제 코드는

1
2
3
4
5
6
7
return traceTemplate.execute("OrderController.request()", new TraceCallBack<>() {  
    @Override  
    public String call() {  
        orderService.orderItem(itemId);  
        return "ok";  
    }  
});

메시지, callback.call() 을 매개변수로 넘기기 때문에 다음과 같이 작성했다. 서비스랑, 리포지토리 단에선 람다를 써서 더 줄여 보자.

서비스, 리포지토리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service  
public class OrderServiceV5 {  
  
    private final OrderRepositoryV5 orderRepository;  
    private final TraceTemplate traceTemplate;  
  
    public OrderServiceV5(OrderRepositoryV5 orderRepository, LogTrace trace) {  
        this.orderRepository = orderRepository;  
        this.traceTemplate = new TraceTemplate(trace);  
    }  
  
    public void orderItem(String itemId) {  
  
        traceTemplate.execute("OrderServiceV5.orderItem()", () -> {  
            orderRepository.save(itemId);  
            return null;  
        });  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Repository  
public class OrderRepositoryV5 {  
  
    private final TraceTemplate traceTemplate;  
  
    public OrderRepositoryV5(LogTrace trace) {  
        this.traceTemplate = new TraceTemplate(trace);  
    }  
  
    public void save(String itemId) {  
  
        traceTemplate.execute("OrderRepositoryV5.save()", () -> {  
            if (itemId.equals("ex")) {  
                throw new IllegalStateException("예외 발생!");  
            }  
            sleep(1000);  
            return null;  
        });  
    }  
  
    private void sleep(long millis) {  
        try {  
            Thread.sleep(millis);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}

자 매우 짧아졌다.

그리고 둘 다 void 가 리턴이기 때문에 return null;을 마지막에 넣어 준다.

익명 내부 클래스에서 Void 타입 리턴 하는 것.

결과 및 정리

결과는 잘 나온다.

지금까지 변하는 코드와 변하지 않는 코드를 분리하고, 더 적은 코드로 로그 추적기를 만들기 위해 고군분투 했다.

템플릿 메서드 패턴, 전략 패턴 그리고 템플릿 콜백 패턴까지 진행하면서 코드를 분리했다.

그리고 최종적으로 템플릿 콜백 패턴을 적용하고 콜백으로 람다를 사용해서 코드 사용도 최소화 할 수 있었다.

한계

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

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

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

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

댓글남기기