스프링 핵심 원리 - 고급편 6

Spring 정리 2023. 1. 25. 21:08

인프런 강의 65일차.

 - 스프링 핵심 원리 - 고급편 (김영한 강사님)

 - 반드시 한번은 정복해야할 쉽지 않은 내용들

 - 크게 3가지 고급 개념을 학습

  1. 스프링 핵심 디자인 패턴

     > 템플릿 메소드 패턴

     > 전략 패턴

     > 템플릿 콜백 패턴

     > 프록시 패턴

     > 데코레이터 패턴

  2. 동시성 문제와 쓰레드 로컬

     > 웹 애플리케이션

     > 멀티쓰레드

     > 동시성 문제

  3. 스프링 AOP

     > 개념, 용어정리

     > 프록시 - JDK 동적 프록시, CGLIB

     > 동작 원리

     > 실전 예제

     > 실무 주의 사항

 - 기타

     > 스프링 컨테이너의 확장 포인트 - 빈 후처리기

     > 스프링 애플리케이션을 개발하는 다양한 실무 팁

 - 타입 컨버터, 파일 업로드, 활용, 쿠키, 세션, 필터, 인터셉터, 예외 처리, 타임리프, 메시지, 국제화, 검증 등등

 

3. 템플릿 메서드 패턴과 콜백 패턴

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

  - ContextV2 는 변하지 않는 템플릿 역할을 한다. 그리고 변하는 부분은 파라미터로 넘어온 Strategy 의 코드를 실행해서 처리한다. 이렇게 다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 콜백(callback)이라 한다.

 

  * 콜백이란?

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

 

  * 자바 언어에서 콜백

   - 자바 언어에서 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다.

   - 자바8부터는 람다를 사용할 수 있다. 자바 8 이전에는 보통 하나의 메소드를 가진 인터페이스를 구현하고, 주로 익명 내부 클래스를 사용했다. 최근에는 주로 람다를 사용한다.

 

  * 템플릿 콜백 패턴

   - 스프링에서는 ContextV2 와 같은 방식의 전략 패턴을 템플릿 콜백 패턴이라 한다. 전략 패턴에서 Context 가 템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다 생각하면 된다.

   - 참고로 템플릿 콜백 패턴은 GOF 패턴은 아니고, 스프링 내부에서 이런 방식을 자주 사용하기 때문에, 스프링 안에서만 이렇게 부른다. 전략 패턴에서 템플릿과 콜백 부분이 강조된 패턴이라 생각하면 된다.

   - 스프링에서는 JdbcTemplate , RestTemplate , TransactionTemplate , RedisTemplate 처럼 다양한 템플릿 콜백 패턴이 사용된다. 스프링에서 이름에 xxxTemplate 가 있다면 템플릿 콜백 패턴으로 만들어져 있다 생각하면 된다.

 

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

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

    > Context = Template

    > Strategy = Callback

package hello.advanced.trace.strategy.code.template;

public interface Callback {
    void call();
}

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

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

 

package hello.advanced.trace.strategy.code.template;

import lombok.extern.slf4j.Slf4j;

@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.code.template.TimeLogTemplate.java

 

package hello.advanced.trace.strategy.code;

import hello.advanced.trace.strategy.code.template.Callback;
import hello.advanced.trace.strategy.code.template.TimeLogTemplate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@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.strategy.code.TemplateCallbackTest.java

  - 별도의 클래스를 만들어서 전달해도 되지만, 콜백을 사용할 경우 익명 내부 클래스나 람다를 사용하는 것이 편리하다. 물론 여러곳에서 함께 사용되는 경우 재사용을 위해 콜백을 별도의 클래스로 만들어도 된다

 

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

  - 템플릿 콜백 패턴을 애플리케이션에 적용

package hello.advanced.trace.callback;

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

  - hello.advanced.trace.callback.TraceCallback.java

  - <T>제네릭을 사용했다. 콜백의 반환 타입을 정의한다.

 

package hello.advanced.trace.callback;

import hello.advanced.trace.TraceStatus;
import hello.advanced.trace.logtrace.LogTrace;

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


}

  - hello.advanced.trace.callback.TraceTemplate.java

  - TraceTemplate 는 템플릿 역할을 한다. execute() 를 보면 message 데이터와 콜백인 TraceCallback callback 을 전달 받는다.

 

package hello.advanced.app.v5;

import hello.advanced.trace.callback.TraceCallback;
import hello.advanced.trace.callback.TraceTemplate;
import hello.advanced.trace.logtrace.LogTrace;
import hello.advanced.trace.template.AbstractTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@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);   //OrderService가 싱글톤이기 때문에 생성자도 한번만 생성됨
    }

    @GetMapping("/v5/request")
    public String request(String itemId) {
        return template.execute("OrderControllerV5.request()", new TraceCallback<>() {
            @Override
            public String call() {
                orderService.orderItem(itemId);
                return "ok";
            }
        });
    }
}

  - hello.advanced.app.v5.OrderControllerV5.java

  - this.template = new TraceTemplate(trace) : trace 의존관계 주입을 받으면서 필요한 TraceTemplate 템플릿을 생성한다.

  - 참고로 TraceTemplate 를 처음부터 스프링 빈으로 등록하고 주입받아도 된다. 이 부분은 선택이다.

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

 

package hello.advanced.app.v5;

import hello.advanced.trace.callback.TraceCallback;
import hello.advanced.trace.callback.TraceTemplate;
import hello.advanced.trace.logtrace.LogTrace;
import hello.advanced.trace.template.AbstractTemplate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@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("OrderServiceV5.orderItem()", () -> {
            orderRepository.save(itemId); //핵심 기능
            return null;
        });
    }
}

  - hello.advanced.app.v5.OrderServiceV5.java

  - template.execute(.., new TraceCallback(){..}) : 템플릿을 실행하면서 콜백을 전달한다. 여기서는 콜백으로 람다를 전달했다.

 

package hello.advanced.app.v5;

import hello.advanced.trace.callback.TraceCallback;
import hello.advanced.trace.callback.TraceTemplate;
import hello.advanced.trace.logtrace.LogTrace;
import hello.advanced.trace.template.AbstractTemplate;
import org.springframework.stereotype.Repository;


@Repository
public class OrderRepositoryV5 {

    private final TraceTemplate template;

    public OrderRepositoryV5(LogTrace trace) {
        this.template = new TraceTemplate(trace);
    }

    public void save(String itemId) {
        template.execute("OrderRepositoryV5.request()", () -> {
            //저장 로직
            if (itemId.equals("ex")) {  //상품 ID 가 ex 면 예외 처리
                throw new IllegalStateException("예외 발생!");
            }
            sleep(1000);    //상품을 저장하는데 1초가 걸린다고 가정
            return null;
        });
    }

    private void sleep(int miilis) {
        try {
            Thread.sleep(miilis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  - hello.advanced.app.v5.OrderRepositoryV5.java

 

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

  - 정상 실행 로그

 

 3.13. 템플릿 콜백 패턴 - 정리

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

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

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

  - 그런데 지금까지 설명한 방식의 한계는 아무리 최적화를 해도 결국 로그 추적기를 적용하기 위해서 원본 코드를 수정해야 한다는 점이다. 클래스가 수백개이면 수백개를 더 힘들게 수정하는가 조금 덜 힘들게 수정하는가의 차이가 있을 뿐, 본질적으로 코드를 다 수정해야 하는 것은 마찬가지이다.

  - 다음부터는 원본 코드를 손대지 않고 로그 추적기를 적용할 수 있는 방법을 알아보자. 그러기 위해서 프록시 개념을 먼저 이해해야 한다.

  - 지금까지 설명한 방식은 실제 스프링 안에서 많이 사용되는 방식이다. xxxTemplate 를 만나면 이번에 학습한 내용을 떠올려보면 어떻게 돌아가는지 쉽게 이해할 수 있을 것이다.