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

Spring 정리 2023. 4. 30. 18:05

인프런 강의 66일차.

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

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

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

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

     > 템플릿 메소드 패턴

     > 전략 패턴

     > 템플릿 콜백 패턴

     > 프록시 패턴

     > 데코레이터 패턴

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

     > 웹 애플리케이션

     > 멀티쓰레드

     > 동시성 문제

  3. 스프링 AOP

     > 개념, 용어정리

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

     > 동작 원리

     > 실전 예제

     > 실무 주의 사항

 - 기타

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

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

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

 

4. 프록시 패턴과 데코레이터 패턴

 4.1. 프로젝트 생성

  - https://start.spring.io
  - 프로젝트 선택 Project : Gradle Project
  - Language : Java
  - Spring Boot : 2.7.0
  - Group : hello
  - Artifact : proxy
  - Name : proxy
  - Package name : hello.proxy
  - Packaging : Jar
  - Java : 11
  - Dependencies : Spring Web, Lombok
* gradle.build 실행 후 아래 오류 발생 시 gradle-wrapper.properties에서 gradle 버전을 6.9 이하로 변경해서 다운로드하자. (gradle-6.9-all.zip)
 > Unable to find method 'org.gradle.api.artifacts.result.ComponentSelectionReason.getDescription()Ljava/lang/String;'. Possible causes for this unexpected error include: Gradle's dependency cache may be corrupt (this sometimes occurs after a network connection timeout.) Re-download dependencies and sync project (requires network)

 

 4.2. 예제 프로젝트 만들기 V1

  - 예제는 크게 3가지 상황으로 만든다.

    > v1 - 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록

    > v2 - 인터페이스 없는 구체 클래스 - 스프링 빈으로 수동 등록

    > v3 - 컴포넌트 스캔으로 스프링 빈 자동 등록

  - 실무에서는 스프링 빈으로 등록할 클래스는 인터페이스가 있는 경우도 있고 없는 경우도 있다.

  - 그리고 스프링 빈을 수동으로 직접 등록하는 경우도 있고, 컴포넌트 스캔으로 자동으로 등록하는 경우도 있다.

  - 이런 다양한 케이스에 프록시를 어떻게 적용하는지 알아보기 위해 다양한 예제를 준비해보자.

  -  v1 : 인터페이스와 구현 클래스 - 스프링 빈으로 수동 등록

package hello.proxy.app.v1;

public interface OrderRepositoryV1 {
    void save(String itemId);
}

 

package hello.proxy.app.v1;

public class OrderRepositoryV1Impl implements OrderRepositoryV1 {
    @Override
    public void save(String itemId) {
        //저장 로직
        if( itemId.equals("ex")){
            throw new IllegalStateException("예외 발생!");
        }
        sleep(1000);
    }

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

 - hello.proxy.app.v1.OrderRepositoryV1.java

 - hello.proxy.app.v1.OrderRepositoryV1Impl.java

 

package hello.proxy.app.v1;

public interface OrderServiceV1 {
    void orderItem(String itemId);
}
package hello.proxy.app.v1;

public class OrderServiceV1Impl implements OrderServiceV1 {

    private final OrderRepositoryV1 orderRepository;

    public OrderServiceV1Impl(OrderRepositoryV1 orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public void orderItem(String itemId) {

    }
}

 - hello.proxy.app.v1.OrderServiceV1.java

 - hello.proxy.app.v1.OrderServiceV1Impl.java

 

package hello.proxy.app.v1;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@RequestMapping     //스프링은 @RequestMapping 혹은 @Controller가 있어야 스프링 컨트롤러로 인식
@ResponseBody
public interface OrderControllerV1 {

    @GetMapping("/v1/request")
    String request(@RequestParam("itemId") String itemId);

    @GetMapping("/v1/no-log")
    String noLog();
}

 - hello.proxy.app.v1.OrderControllerV1.java

 - @RequestMapping : 스프링MVC는 타입에 @Controller 또는 @RequestMapping 애노테이션이 있어야 스프링 컨트롤러로 인식한다. 그리고 스프링 컨트롤러로 인식해야, HTTP URL이 매핑되고 동작한다. 이 애노테이션은 인터페이스에 사용해도 된다.

 - @ResponseBody : HTTP 메시지 컨버터를 사용해서 응답한다. 이 애노테이션은 인터페이스에 사용해도 된다.

 - @RequestParam("itemId") String itemId : 인터페이스에는 @RequestParam("itemId") 의 값을 생략하면 itemId 단어를 컴파일 이후 자바 버전에 따라 인식하지 못할 수 있다.

 - 인터페이스에서는 꼭 넣어주자. 클래스에는 생략해도 대부분 잘 지원된다.

 - 코드를 보면 request() , noLog() 두 가지 메서드가 있다.

 - request() 는 LogTrace 를 적용할 대상이고, noLog() 는 단순히 LogTrace 를 적용하지 않을 대상이다.

 

package hello.proxy.app.v1;

public class OrderControllerV1Impl implements OrderControllerV1 {

    private final OrderServiceV1 orderService;

    public OrderControllerV1Impl(OrderServiceV1 orderService) {
        this.orderService = orderService;
    }

    @Override
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @Override
    public String noLog() {
        return "ok";
    }
}

 - hello.proxy.app.v1.OrderControllerV1Impl.java

 - 컨트롤러 구현체이다. OrderControllerV1 인터페이스에 스프링MVC 관련 애노테이션이 정의되어 있다.

 

package hello.proxy.config;

import hello.proxy.app.v1.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppV1Config {

    @Bean
    public OrderControllerV1 orderControllerV1(){
        return new OrderControllerV1Impl(orderServiceV1());
    }

    @Bean
    public OrderServiceV1 orderServiceV1() {
        return new OrderServiceV1Impl(orderRepositoryV1());
    }

    @Bean
    public OrderRepositoryV1 orderRepositoryV1() {
        return new OrderRepositoryV1Impl();
    }
}

 - hello.proxy.config.AppV1Config.java

 - 스프링 Bean 수동 등록 config

 

package hello.proxy;

import hello.proxy.config.AppV1Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의 (설정 패키지 하위에서 컴포넌트 스캔 후 Bean을 자동등록한다)
public class ProxyApplication {

	public static void main(String[] args) {
		SpringApplication.run(ProxyApplication.class, args);
	}

}

 - hello.proxy.ProxyApplication.java

 - @Import(AppV1Config.class) : 클래스를 스프링 빈으로 등록한다.

 - 여기서는 AppV1Config.class 를 스프링 빈으로 등록한다.

 - 일반적으로 @Configuration 같은 설정 파일을 등록할 때 사용하지만, 스프링 빈을 등록할 때도 사용할 수 있다.

 - @SpringBootApplication(scanBasePackages = "hello.proxy.app") : @ComponentScan 의 기능과 같다.

 - 컴포넌트 스캔을 시작할 위치를 지정한다. 이 값을 설정하면 해당 패키지와 그 하위 패키지를 컴포넌트 스캔한다.

 - 이 값을 사용하지 않으면 ProxyApplication 이 있는 패키지와 그 하위 패키지를 스캔한다. 

 

 * 주의

  > @Configuration 은 내부에 @Component 애노테이션을 포함하고 있어서 컴포넌트 스캔의 대상이 된다.

  > 따라서 컴포넌트 스캔에 의해 hello.proxy.config 위치의 설정 파일들이 스프링 빈으로 자동 등록 되지 않도록 컴포넌스 스캔의 시작 위치를 scanBasePackages=hello.proxy.app 로 설정해야 수동 등록이 가능하다.

 

 4.2. 예제 프로젝트 만들기 V2

  - v2 : 인터페이스 없는 구현 클래스를 스프링 빈으로 수동 등록

package hello.proxy.app.v2;

public class OrderRepositoryV2 {
    public void save(String itemId) {
        //저장 로직
        if( itemId.equals("ex")){
            throw new IllegalStateException("예외 발생!");
        }
        sleep(1000);
    }

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

 - hello.proxy.app.v2.OrderRepositoryV2.java

 

package hello.proxy.app.v2;

public class OrderServiceV2 {

    private final OrderRepositoryV2 orderRepository;

    public OrderServiceV2(OrderRepositoryV2 orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void orderItem(String itemId) {

    }
}

 - hello.proxy.app.v2.OrderServiceV2.java

 

package hello.proxy.app.v2;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Slf4j
@RequestMapping
@ResponseBody
public class OrderControllerV2 {

    private final OrderServiceV2 orderService;

    public OrderControllerV2(OrderServiceV2 orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/v2/request")
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @GetMapping("/v2/no-log")
    public String noLog() {
        return "ok";
    }
}

 - hello.proxy.app.v2.OrderControllerV2.java

 - @Controller 를 사용하지 않고, @RequestMapping 애노테이션을 사용했다. 그 이유는 @Controller 를 사용하면 자동 컴포넌트 스캔의 대상이 되기 때문이다. 여기서는 컴포넌트 스캔을 통한 자동 빈 등록이 아니라 수동 빈 등록을 하는 것이 목표다. 따라서 컴포넌트 스캔과 관계 없는 @RequestMapping 를 타입에 사용했다.

 

package hello.proxy.config;

import hello.proxy.app.v2.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppV2Config {

    @Bean
    public OrderControllerV2 orderControllerV2(){
        return new OrderControllerV2(orderServiceV2());
    }

    @Bean
    public OrderServiceV2 orderServiceV2() {
        return new OrderServiceV2(orderRepositoryV2());
    }

    @Bean
    public OrderRepositoryV2 orderRepositoryV2() {
        return new OrderRepositoryV2();
    }
}

 - hello.proxy.config.AppV2Config.java

 - App별로 Config를 가져다 사용하도록 변경

 - 기존: @Import(AppV1Config.class)

 - 변경: @Import({AppV1Config.class, AppV2Config.class})

 - @Import 안에 배열로 등록하고 싶은 설정파일을 다양하게 추가할 수 있다.