3. 템플릿 콜백 패턴 - midoBanDev/design-pattern-java GitHub Wiki

템플릿 콜백 패턴

콜백이란?

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

자바에서의 콜백

  • 자바 8 이전에는 하나의 메서드를 가지는 인터페이스를 구현하고, 주로 익명 내부 클래스를 사용했다.
    자바 8 이후에는 람다를 주로 사용한다.

템플릿 콜백 패턴

  • 스프링에서는 Context라는 템플릿에 Strategy 파라미터를 넘기는 방식의 전략 패턴을 템플릿 콜백 패턴이라고 한다.
    클라이언트에서 Strategy를 실행하는 것이 아니라 Context에 Strategy라는 콜백을 넘기면 Context뒤에서 Strategy가 실행된다.
    참고로 템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에 스프링 안에서만 부르는 용어이다.
  • 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라 생각하면 된다.

스프링 콜백 패턴의 사용

  • JdbcTemplate, RestTemplate, TransactionTemplate, RedisTemplate 등 다양한 템플릿 콜백 패턴이 사용된다.

템플릿 콜백 패턴 흐름

image

Code(연습용)

템플릿 코드

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

콜백 인터페이스

public interface CallBack {
	public void call();
}

템플릿 콜백 실행

@Slf4j
public class TemplateCallbackTest {

	/**
	 * 템플릿 콜백 패턴 - 익명 내부 클랙스
	 */
	@Test
	void callbackV1() {
		TimeLogTemplate logTemplate = new TimeLogTemplate();
		logTemplate.execute(new CallBack() {
			
			@Override
			public void call() {
				log.info("비즈니스 로직1 실행");
			}
		});
		logTemplate.execute(new CallBack() {
			
			@Override
			public void call() {
				log.info("비즈니스 로직2 실행");
			}
		});
	}
	
	/**
	 * 템플릿 콜백 패턴 - 람다
	 */
	@Test
	void callbackV2() {
		TimeLogTemplate logTemplate = new TimeLogTemplate();
		logTemplate.execute(() -> log.info("비즈니스 로직1 실행"));
		logTemplate.execute(() -> log.info("비즈니스 로직2 실행"));
	}
}

Code(실전용)

템플릿 코드

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

콜백 인터페이스

  • 인터페이스 1개의 함수만 존재하는 경우 람다를 사용할 수 있다.
  • 람다는 함수형 인터페이스에만 사용가능한데, 인터페이스에 1개의 메서드가 존재하면 자동으로 함수형 인터페이스가 된다.
// 반환 타입이 다양할 수 있기 때문에 제너럴을 사용하였다.
public interface TraceCallback<T> {
	T call();
}

템플릿 콜백 패턴을 적용한 Controller

@RestController
public class OrderControllerV5 {

	private final OrderServiceV5 orderService;
	private final TraceTemplate traceTemplate;
	
	/**
	 * TraceTemplate 를 빈에 등록한 후 싱글톤으로 사용하자.
	 * TraceTemplate 코드를 보면 생성자에서 LogTrace 를 주입받는다.
	 * 1. 생성자를 통해 Service와 LogTrace를 주입 받은 후 빈에 등록한다. 
	 * 2. TraceTemplate 에 @Component 를 선언하여 자동으로 빈에 등록해서 사용한다.
	 *    이러면 LogTrace도 자동 주입 받는다. (TraceTemplate 생성자를 통해)
	 *  
	 * 1번으로 사용하는 이유는 테스트 시 유리한 면이 있다.
	 * 하지만 1번을 사용하든 2번을 사용하든 전혀 문제 없음.
         *
         * LogTrace가 빈에 등록되어 있으면 이 처럼 생성자를 통해 주입해 줄 수 있다.(당연한 건데 어색함, 개념 챙기자.)
	 */
	public OrderControllerV5(OrderServiceV5 orderService, LogTrace logTrace) {
		this.orderService = orderService;
		this.traceTemplate = new TraceTemplate(logTrace);
	}

	/**
	 * 익명 내부 클래스 사용
	 */
	@GetMapping("/v5/request")
	public String request(String itemId) {
		return traceTemplate.execute("OrderControllerV5.request()", new TraceCallback<>() {
			@Override
			public String call() {
				orderService.orderItem(itemId);
				return "ok";
			}
		});
	}

	/**
	 * 람다 사용
	 */
	@GetMapping("/v5.1/request")
	public String request2(String itemId) {
		return traceTemplate.execute("OrderControllerV5.request()", () -> {
			orderService.orderItem(itemId);
			return "ok";
		});
	}
}