[Spring] 기본_빈 스코프

1. 빈 스코프란?

  • 스프링 빈은 기본적으로 싱글톤 스코프로 생성되어 스프링 컨테이너가 시작할 때 생성되어 종료될 때까지 유지된다.
    • 싱글톤: 디폴트 스코프로, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
    • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존 관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
    • 웹 관련 스코프
      1) request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
      2) session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
      3) application: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프

[ 컴포넌트 스캔 등록 ]

// 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}

// 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() { return new HelloBean(); }

2. 프로토타입 스코프

2-1. 싱글톤 타입 빈 요청

  • 싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환

2-2. 프로토타입 빈 요청

  1. 프로토타입 빈 요청
  2. 스프링DI 컨테이너에서 새로운 빈 생성 + 의존성 주입
  3. 생성된 빈 반환 -> 관리X
  4. 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환
  • 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리함
    • @PreDestroy 같은 종료 메서드가 호출되지 않음
    • 종료 메서드는 클라이언트가 직접 해야함

2-3. 싱글톤 빈에서 프로토타입 빈 사용시 문제점

  1. 싱글톤 빈(clientBean)은 스프링 컨테이너 생성 시점에 함께 생성되고, 의존 관계 주입도 발생
  2. clientBean은 의존관계 자동주입 사용하므로 주입 시점에 스프링 컨테이너에 PrototypeBean을
    요청
  3. 스프링 컨테이너는 PrototypeBean을 생성해서 clientBean에 반환
  4. PrototypeBean은 clientBean의 내부 필드에 참조값 보관
  5. clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다
  6. 클라이언트A가 clientBean.logic()을 호출하면 count = 1, 클라이언트B가 clientBean.logic()을 호출하면 count = 2가 나옴
  • 내가 원하는 것: 클라이언트마다 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성되는 것을 원함

2-4. Provider로 문제 해결

  • 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청
  • ObjectProvider: 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것
    • Provider보다 우선적으로 사용(스프링이 아닌 다른컨테이너에서도 사용해야한다면 Provider 사용)
    • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL)
    • 스프링에 의존적 + 이외의 기능 제공
public class SingletonBean {
	@Autowired
	private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
		PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
		prototypeBean.addCount();
		return prototypeBean.getCount();
	}   
}
  • Provider: 자바 표준
    • provider의 get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 (DL)
    • 별도의 라이브러리가 필요
    • 자바 표준으로 다른 컨테이너에서도 사용 가능
public class SingletonBean {
    @Autowired
    private Provider<PrototypeBean> prototypeBeanObjectProvider;

    public int logic() {
	    PrototypeBean object = prototypeBeanObjectProvider.get();
        object.addCount();
        return object.getCount();
	}   
}

3. 웹 스코프

3-1. 웹 스코프란?

  • 웹 환경에서만 동작하는 스코프
  • 스프링이 해당 스코프의 종료 시점까지 관리하여 종료 메서드가 호출됨

3-2. 웹 소코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프로, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리됨
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: ServletContext와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

3-3. requset 스코프 예제

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {

    private String uuid;
    private String requestURL;

    // requestURL: 나중에 별도로 세팅
    // 빈이 생성되는 시점에는 알 수 없음
    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "] [" + requestURL + "] [" + message + "]");
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();  // 랜덤으로 uuid생성
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }

}
  • 로그를 출력하기 위한 클래스
  • 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸됨
  • 이 빈이 생성되는 시점에 자동으로 초기화 메서드를 사용해서 uuid 저장
  • 이 빈은 HTTP 요청 당 하나씩 생성되므로, 다른 HTTP 요청과 구분할 때 uuid 사용
  • 이 빈이 소멸되는 시점에 소멸 전 메서드로 종료 메시지 남김
  • requestURL 은 이 빈이 생성되는 시점에는 알 수 없기 때문에 setter로 입력받음
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();  //원하는 주입 시점에 주입할 수 있음
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("test id");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;
//    private final MyLogger myLogger;

    public void logic(String id) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

[ 과정 ]

  1. MyLogger myLogger = myLoggerProvider.getObject();에서 처음만들어짐
  2. 그때, init()이 호출되면서 uuid를 request랑 연결
  3. setrequestURL 담아놓고 로그찍기
  4. 로그찍는 순간에는 이미 uuid, requestURL있기 때문에 에러없이 찍힘
    수많은 요청이 와도 요청마다 따로 객체 관리
  • 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않기 때문에 MyLogger 빈이 아직 만들어지기 전이다.
  • 이를 해결하기 위해 Provider를 사용해서 request 스코프 빈의 생성을 지연

4. 스코프와 프록시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}
  • 프록시 방식으로, MyLogger의 가짜 프록시 클래스를 만들어두고, HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해둘 수 있음
@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;

    // proxy 설정했다면
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody  // 화면 없이 문자 바로 리턴 -> 문자 그대로 응답보냄
    public String logDemo(HttpServletRequest request) {// HttpServletRequest: 고객 요청정보 받을 수 있음
        // MyLogger myLogger = myLoggerProvider.getObject();
        String requestURL = request.getRequestURI().toString();

        System.out.println("myLogger = " + myLogger.getClass());
        // myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$ef899579 -> 얘가 Provider처럼 동작하는 것
        // 진짜 myLogger가 아니라 껍데기만 가져다놓은것
        // 의존관계 주입도 가짜 프록시 객체 주입됨
        // ac.getBean("myLogger", MyLogger.class)를 조회해도 프록시 객체가 조회됨

        // 기능을 호출하는 시점에 진짜 로직을 찾아 동작
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");  // 이거도 가짜 프록시 객체의 메서드를 호출한 것
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {

    // proxy 설정했다면
    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log("service id: " + id);
    }
}

좋은 웹페이지 즐겨찾기