[SpringBoot 핵심 원리] 템플릿 메소드 패턴과 콜백 패턴 (3) + 템플릿 콜백 패턴 / 전략 패턴 / 제네릭

윤경·2021년 12월 28일
0

Spring Boot

목록 보기
65/79
post-thumbnail

[12] 템플릿 콜백 패턴 - 시작

ContextV2는 변하지 않는 템플릿 역할을 한다.
그리고 변하는 부분은 파라미터로 넘어온 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가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.


[13] 템플릿 콜백 패턴 - 예제

: 템플릿 콜백 패턴 구현하기
ContextV2와 내용은 같고 이름만 다르다.

  • ContextTemplate
  • StrategyCallback

✔️ Callback.java (callback 인터페이스)

public interface Callback {
    void call();
}

➡️ 콜백 로직을 전달할 인터페이스

✔️ TimeLogTemplate.java

@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.java

@Slf4j
public class TemplateCallbackTest {

    /**
     * 템플릿 콜백 패턴 - 익명 내부 클래스
     */
    @Test
    void callbackV1() {
        TimeLogTemplate template = new TimeLogTemplate();
        template.execute(new Callback() {   // 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 실행"));    // () -> log.info("비즈니스 로직1 실행") -> 이 부분이 템플릿 뒤쪽에서 실행됨 (callback)
        template.execute(() -> log.info("비즈니스 로직2 실행"));
    }
}

별도의 클래스를 만들어 전달해도 되지만, 콜백을 사용할 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다.

물론, 여러 곳에서 함께 사용되는 경우 재사용을 위해 콜백을 별도의 클래스로 만들어도 된다.


[14] 템플릿 콜백 패턴 - 적용

: 템플릿 콜백 패턴 애플리케이션에 적용하기

✔️ TraceCallback.java (interface)

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

✔️ TraceTemplate.java

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> (📌) 제네릭을 사용하였고 반환 타입을 정의한다.

⚠️ v5에 복사하고 v5로 링크와 이름을 수정한 뒤 한 번 실행시켜보아야 한다. (쌓아두면 나중에 큰 일이 될 수 있음)

✔️ OrderControllerV5.java

@RestController // @Controller + @ResponseBody
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<>() {
            @Override
            public String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        });
    }
}
  • private final TraceTemplate template;
    : 계속 생성하는 것이 번거로우므로 이렇게 한 번 생성해두면 어차피 OrderControllerV5가 싱글톤이므로 생성자는 한 번 호출되므로 편리하다.

  • this.template = new TraceTemplate(trace)
    : trace 의존 관계를 주입 받으면서 필요한 TraceTemplate 템플릿을 생성한다.
    (📌 참고로 TraceTemplate를 처음부터 스프링 빈으로 등록하고 주입 받아도 된다. 이건 알아서 선택해 사용할 것)

  • template.execute(.., new TraceCallback(){..})
    : 템플릿을 실행하면서 콜백을 전달한다.
    여기서는 콜백으로 익명 내부 클래스를 사용했다.

✔️ OrderServiceV5.java

@Service    // @Service 안에 @Component가 있어 자동으로 컴포넌트 스캔의 대상이 됨(자동으로 스프링 빈 등록)
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;
        });
    }
}
  • template.execute(.., new TraceCallback(){..})
    : 템플릿을 실행하면서 콜백을 전달한다.
    여기서는 콜백으로 람다를 전달했다.

✔️ OrderRepositoryV5.java

@Repository // 안에 @Component가 있어 자동으로 컴포넌트 스캔 대상이 됨(스프링 빈 등록)
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")) {   // (다양한 예제를 위해)"ex"라는 것이 넘어오면 예외 발생시킬
                throw new IllegalStateException("예외 발생!");
            }
            sleep(1000);    // 상품을 저장하는데 1초 정도 걸린다고 가정

            return null;
        });
    }
...

➡️ 나머지 부분은 V4와 동일

실행 결과

실행 결과


📌 템플릿 콜백 패턴

(Template Callback Pattern)
: 전략 패턴의 변형으로 DI(Dependency Injection) 의존성 주입에서 사용하는 특별한 전략 패턴

즉, 템플릿 콜백 패턴은 (📌)전략 패턴에 익명 내부 클래스를 가미해 사용하는 방법이다.

📌 전략 패턴

(Strategy Pattern)

전략 패턴을 구성하는 3가지 요소

  1. 전략 메소드를 가진 전략 객체
  2. 전략 객체를 사용하는 컨텍스트(전략 객체의 사용자/소비자)
  3. 전략 객체를 생성해 컨텍스트에 주입하는 클라이언트(제 3자, 전략 객체의 공급자)

클라이언트는 다양한 전략 중 현재 상황에 적합한 전략을 생성해 컨텍스트에게 전략 객체를 주입한다.

즉, 다양하게 전략을 변경하며 컨텍스트에게 주입하는 것
➡️ 클라이언트가 전략을 생성해 전략을 실행할 컨텍스트에게 주입하는 패턴


📌 제네릭

(Generic)
: 데이터 타입을 일반화하다.

즉, 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것

특정 타입을 미리 지정하지 않고, 필요에 의해 지정할 수 있도록 하는 일반타입을 말한다.

정확히 말하자면 타입의 경계를 지정하고, 컴파일 때 해당 타입으로 캐스팅하여 매개변수화 된 유형을 삭제하는 것이다.

제네릭의 장점

  1. 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
  2. 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다.
    (관리가 편함)
  3. 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.

표 출처

이것이 암묵적인 규칙이다. <V><Val>로 해도 되지만 통상적으로 이렇게 많이 쓰기 때문에 대단한 경우가 아니면 표의 내용처럼 사용하자.


디자인 콜백 패턴 참고
제네릭 참고

profile
개발 바보 이사 중

0개의 댓글