Spring boot

[기본] 빈 스코프

메모

 

intellij 단축키

  ctrl + alt + N : 인라인 리턴

 


빈 스코프란?

 

스코프(scope, 범위)란 빈이 존재할 수 있는 범위를 뜻한다.

 

스프링이 지원하는 스코프

  1. 싱글톤: 기본, 컨테이너의 시작부터 종료까지 유지됨
  2. 프로토타입: 빈의 생성과 의존관계 주입까지만 관여함
  3. 웹 관련: request- 웹 요청 들어올때부터 나갈때까지, session- 웹 세션의 생성부터 종료까지, application- 웹의 서블릿 컨텍스트와 같은 범위

 

스코프를 지정하는 방법

@Scope("prototype") 애노테이션을 클래스에 붙인다.

작성하지 않으면 기본인 싱글톤으로 간주된다.

 


프로토타입 스코프

 

싱글톤 스코프 빈은 항상 같은 인스턴스를 반환한다.

프로토타입 스코프는 조회시마다 항상 새로운 인스턴스를 생성해 반환한다.

 

즉, 스프링 컨테이너가 프로토타입 빈 생성, DI, 초기화까지만 처리한다.

그 후 빈의 관리 책임은 빈을 받은 클라이언트에 있다.

 

특징

- 요청시마다 새로 생성된다.

- 스프링 컨테이너는 빈 생성, DI, 초기화만 한다.

- 따라서 종료메서드는 호출되지 않는다.

- 클라이언트가 직접 종료메서드를 호출해야한다.

 


프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

 

만약 빈을 요청하여 초기값이 0인 count값을 +1 시키는 로직이 있을 때,

프로토타입 빈은 항상 0 -> 1이 된다.

반면 싱글톤빈은 0 -> 1 -> 2 -> .... 일 것이다.

 

그러나 싱글톤 빈을 생성할 때 프로토타입 빈을 주입받는다면 해당 프로토타입빈은 싱글톤 빈과 함께 유지된다.

따라서 count값은 0 -> 1 -> 2 ... 가 된다.

 

프로토 타입 빈을 사용하는 목적과 맞지 않기 때문에 사용자는 이런 동작을 원하지 않았을 것이다.

 


프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

 

싱글톤 빈과 함께 사용할 때 매번 새로운 프로토타입 빈을 생성하는 방법은 무엇이 있을까?

 

1. 스프링 컨테이너에 요청

  싱글톤 클라이언트 빈에서 ApplicationContext를 주입받아 매번 prototype bean을 요청하여 사용한다.

 

 

이처럼 의존관계를 주입받는게 아니라, 직접 의존관계를 찾는것을 Dependency Lookup(DL, 의존관계 조회)라 한다.

 

그러나 이렇게 스프링 application context를 전체를 주입하면, 스프링 컨테이너에 종속적이 된다.

application context에 많은 기능이 있기 때문에 후에 자칫 잘못 사용할 가능성도 있어 좋은 방식이 아니다.

 

딱 DL 기능만 제공하는 것이 필요하다.

 

2. ObjectFactory, ObjectProvider

DL 서비스를 제공하는 ObjectFactory 에 여러 편의 기능을 추가해 ObjectProvider가 만들어졌다.

 

클라이언트 빈에 ApplicationContext 대신 ObjectProvider<PrototypeBean>을 주입받는다.

.getObject() 메서드를 통해 새로운 빈을 DL한다.

 

지금 필요한 DL 기능만 제공하므로 단위테스트를 만들기 쉬워진다.

또한 별도의 라이브러리가 필요하지 않아 간편하다.

 

3. JSR-330 Provider

javax.inject:javax.inject:1 라이브러리의 JSR-330 을 사용하는 방법도 있다.

위 라이브러리를 gradle에 추가해아한다.

주석처리된 부분은 Object Provider

.get() 메서드를 통해 새 프로토타입 빈을 받는다.

자바 표준이어서 스프링 외 컨테이너에 사용 가능하고 역시 기능이 단순해 단위테스트, mock 코드 작성이 쉬워진다.

 


정리

 

프로토타입 빈은 의존관계 주입이 완료된 새 객체가 필요할 때 사용한다.

실무에서는 대부분 싱글톤을 사용하기 때문에 프로토타입 빈은 매우 드물게 사용된다.

Provider는 프로토타입 뿐 아니라 다른 DL이 필요한 곳에서 사용 가능하다.

 

참고: 같은 기능을 제공할 때 자바 vs 스프링 중 어느 것을 선택해야 할까?

ObjectProvider는 스프링, JSR330Provider는 자바에서 지원한다.

이처럼 비슷한 기능을 제공하면 어떤 것을 사용해야할까?

 

JPA의 경우 자바 표준이 승리했다.

하이버네이트가 JPA 표준에 들어가버렸기 때문에 자바 표준을 사용하면 된다.

 

그러나 스프링은 애매한 상태이다.

스프링과 비슷한 개념을 자바에서 따라 만들었다. (컨테이너 Autowired 등등..)

그러나 표준 기능이 스프링것보다 불편해서 사람들이 잘 안넘어가는 상황이다.

 

시대가 지나면 바뀔수도 있지만 사실상 스프링이 지금 기술 표준이다.

컨테이너기술 등은 거의 스프링것을 사용한다.

 

즉, 스프링 vs 자바 표준 선택해야 할 때

JPA 관련된 것은 자바 표준것을 사용하고 스프링의 경우 기능을 보고 편리한 쪽을 선택하자

만약 기능이 비슷하다면 스프링에서도 자바표준것을 권장하니 그대로 사용하면 된다. (대표적으로 @PostConstruct, @PreDistroy)

 

 


웹 스코프

특징

- 웹 환경에서만 동작한다.

- 해당 스코프의 종료시점까지 관리한다. 즉 종료 메서드 호출된다.

 

웹 스코프의 종류에는 request, session, application, websocket 가 있다.

범위만 다르지 동작 방식은 유사하므로 request 만을 대표로 알아보자.

 


request 스코프 예제 만들기

 

'org.springframework.boot:spring-boot-starter-web' 을 gradle에 추가한다.

위 라이브러리는 내장 톰켓 서버를 활용해 웹서버와 스프링을 실행시킨다.

 

웹 라이브러리가 추가되면 웹 설정이 포함된 AnnotationConfigServletWebServerApplicationContext 기반으로 애플리케이션이 구동된다.

  • 참고) Servlet은 서버 쪽에서 실행되면서 클라이언트의 요청에 따라 동적으로 서비스를 제공하는 자바 클래스

 

request 스코프는 여러 http 요청 중 로그를 구분할 때 사용하기 좋다.

 

@Scope(vlaue = "request")를 로그를 찍을 MyLogger클래스에 붙인다. 이제 이 빈은 http 요청당 하나씩 생성되고, 요청 종료화 함께 소멸된다.

메세지를 출력할 log메서드와 시작 메서드, 종료 메서드를 작성한다.

 

내부 변수로 uuid와 requestURL을 저장해 다른 HTTP 요청과 구분할 때 사용한다.

 

이제 localhost:8080/log-demo 요청을 관리할 컨트롤러를 만든다.

HttpServletRequest으로 자바에서 제공하는 표준 서블릿 규약에 의한 http request를 받을 수 있다.

 

참고) requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉 터나 서블릿 필터 같은 곳을 활용하는 것이 좋다.

 

request scope를 사용함으로써 웹과 관련 없는 서비스 계층까지 웹 정보를 파라미터로 넘기지 않아도 된다. -> 유지보수 측면에서 좋다.

 

서버를 실행하면 이런 오류가 발생한다.

Error creating bean with name 'myLogger': Scope 'request' is not active for the 
current thread; consider defining a scoped proxy for this bean if you intend to 
refer to it from a singleton;

 

scope가 리퀘스트이다 = 생존범위는 고객이 요청하고 나갈때까지이다.

그런데 스프링 뜰 때 리퀘스트 스코프인 myLogger를 DI하니 에러가 발생한다.

스프링을 띄울 때가 아니라 고객요청이 왔을 때로 DI를 미뤄야함 -> provider로 해결하자

 


스코프와 Provider

 

앞서 배운 ObjectProvider를 활용하여 컨트롤러에서 MyLogger가 생성되는 시점을 미루자

ObjectProvider.getObject() 호출 시점에 이미 http 요청이 진행중이므로 오류가 발생하지 않는다.

 


스코프와 프록시

 

proxy는 대리 라는 뜻이다.

proxy를 활용하면 provider를 사용하도록 코드를 수정하지 않고 오류를 해결할 수 있다.

proxyMode = ScopedProxyMode.Target_CLASS 만 추가해주면 된다.

적용 대상이 인터페이스라면 .INTERFACES로 해주자.

 

프록시는 가짜 MyLogger 객체를 만들어 다른 빈에 주입해 둔다.

컨트롤러에서 MyLogger 객체를 찍어보면 다음과 같다.

myLogger에 Spring의 CGLIB가 붙어있는 것을 확인할 수 있다.

 

CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.

가짜 프록시 객체는 요청이 들어오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있고, 싱글톤처럼 작용한다.

 

클라이언트는 다형성에 의해 프록시를 원본 객체로 대체할 수 있으며 싱글톤 빈처럼 request scope를 사용할 수 있다.

 

주의) 이런 싱글톤이 아닌 스코프류는 꼭 필요한 곳에서만 사용하자. 주로 대놓고 사용하기보다 백그라운드에서 사용되는 편이다.

 

'Spring boot' 카테고리의 다른 글

[HTTP] URI와 웹브라우저 요청 흐름  (0) 2021.09.29
[HTTP] 인터넷 네트워크  (2) 2021.09.22
[기본] 빈 생명주기 콜백  (0) 2021.08.24
[기본] 의존관계 자동 주입  (2) 2021.08.17
[기본] 컴포넌트 스캔  (0) 2021.08.16