스프링 빈의 scope는 2가지 종류가 있다.
- Singleton(싱글톤) : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- ProtoType(프로토타입) : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
싱글톤 스코프는 스프링 빈의 디폴트이고 스프링 컨테이너가 싱글톤으로 관리한다는 것을 예전 포스팅에서 알아보았다.
https://programmingrecoding.tistory.com/55?category=846369
프로토 타입 스코프에 대해서 자세히 알아보자.
프로토 타입 스코프
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스 스프링 빈을 반환한다. 반면 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 생성한 후 클라이언트에게 빈을 반환하고 그 이후엔 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDestory 같은 종료 메서드가 호출되지 않는다.
간단하게 테스트 코드로 프로토타입 스코프 빈의 생성과 소멸을 살펴보자.
31줄의 ProtoTypeBean이라는 빈을 만드는데 @Scope("prototype") 애노테이션을 이용하여 빈으로 등록될 때 프로토타입 스코프 속성을 가지게 해주었다. 생성과 소멸을 보기 위해서 @PostContruct와 @PreDestory 애노테이션을 이용하여 init메서드와 close메서드를 각각 만들어주었다.
19번줄에서 스프링 컨테이너를 생성하고 getBean하여 빈을 가지고 와서 2번을 찍어보았다.
분명 로그 마지막줄에 Closing~~ 로그가 찍혀서 스프링 컨테이너가 소멸이 되었지만 close메서드는 호출되지 않았다.
일반적인 싱글톤 스코프의 스프링 빈 생명주기는 다음과 같다.
"스프링 컨테이너 생성" -> "스프링 빈 생성" -> "의존관계 주입"
-> "초기화 콜백" -> "사용" -> "소멸전 콜백" -> "스프링 종료"
하지만, 프로토타입 스코프의 스프링 빈은 초기화 콜백 후, 스프링 컨테이너에서 관리하지 않기 때문에 소멸전 콜백의 과정은 일어나지 않는 것이다. 만약(거의 일어나지 않겠지만), 프로토타입 스코프인 빈의 소멸전 콜백을 사용해야 한다면 close() 메서드를 직접 호출해서 사용해야 한다.
또 한, 각각 protoTypeBean1과 protoTypeBean2는 싱글톤처럼 한개의 빈을 공유한 것이 아닌 서로 다른 빈 객체이다. 로그를 보면 마지막에 주소값이 서로 다른것을 확인할 수 있다. 스프링 컨테이너에게 요청할 때마다 새로 생성되는 것이다.
싱글톤에서 프로토타입 빈을 사용할 때 (문제점)
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 그런데, 문제는 싱글톤 빈과 프로토타입 빈을 함께 사용하면 의도한대로 잘 동작하지 않는다..
김영한님의 스프링 핵심 원리 - 기본편 강의자료에 너무나도 이해가 쉽게 영한님이 그려놓으신 그림이 있어서 이해를 위해 사진을 가져왔다.
출처 : <인프런> 스프링 핵심 원리 기본편 - 김영한
1. clientBean은 의존관계 자동주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
2. 스프링 컨테이너는 프로토타입 빈을 생성해서 'clientBean'에 반환한다. 프로토 타입 빈의 count필드 값은 0이다. 이제 프로토타입 빈은 스프링 컨테이너의 손을 떠났고 'clientBean'에서 관리한다.(프로토타입 빈을 내부 필드에 보관한다. 정확히는 참조값을 보관한다.)
3. 클라이언트 A는 clientBean을 스프링 컨테이너에 요청하여 받는다. clientBean은 싱글톤이므로 항상 같은 clientBean이 반환된다.
4. 클라이언트 A는 'clientBean.logic()'을 호출한다.
5. clientBean은 prototypeBean의 'addCount()'를 호출해서 프로토타입 빈의 count를 증가한다. count는 1이 된다.
6. 클라이언트 B는 clientBean을 스프링 컨테이너에 요청하여 받는다. clientBean은 싱글톤이므로 항상 같은 clientBean이 반환된다.
7. clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에서 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지 사용할 때마다 새로 생성되는 것이 아니다.
8. 클라이언트 B는 'clientBean.logic()'을 호출한다.
9. clientBean은 prototypeBean의 'addCount()'를 호출해서 프로토타입 빈의 count를 증가한다. count는 2이 된다.
위의 그림을 코드로 이해해보자.
싱글톤빈의 @Scope 애노테이션은 디폴트 값이기 때문에 생략해주었다. SingletonBean에서 PrototypeBean을 의존관계 자동주입한다.
스프링 컨테이너가 만들어지고, SingletonBean의 의존관계 자동주입된 PrototypeBean은 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다. 빈의 스코프가 프로토타입이기때문에 우리는 singleton1과 singleton2객체의 logic메서드를 각각 호출한 결과가 같은 값인 1임을 기대했지만 logic1은 1, logic2는 2라는 결과를 얻었다.
위에서 설명하였지만, 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에서 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지 사용할 때마다 새로 생성되는 것이 아니다.
우리는 어떻게 기대하는 값을 얻어낼 수 있을까?
ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup)서비스를 제공하는 인터베이스인 ObjectFactory나 ObjectProvider를 사용하면 된다.
우리는 외부에서 의존관계를 주입해주는 것을 Dependency Injection이라고 불렀다. 보통 @Autowired 애노테이션을 사용하여 스프링 컨테이너가 생성된 후, 의존관계 자동주입을 할 수 있도록 해주었었다.
Dependency Lookup은 의존관계를 내부에서 주입받는 것이 아니라 직접 필요한 시점에 의존관계를 찾는 것을 말한다.
이와 같이 의존관계 자동주입을 할 PrototypeBean을 ObjectProvider혹은 ObjectFactory인터페이스로 만들고, 실제 이 빈이 사용되는 로직에서 35번째 줄과 같이 getObject메서드를 이용해서 불러오면 된다. 자동으로 의존관계를 주입해주는 것이 아니라 클라이언트가 필요시에 빈을 가져와서 의존관계를 찾았기 때문에 DI가 아니라 DL 인 것이다. 이 때, DL을 해줌으로써 우리는 빈의 생성을 지연할 수 있었다.
ObjectFactory는 getObject기능만 있는 단순한 버전이고 ObjectProvider는ObjectFactory상속, 옵션, 스트림처리 등 편의 기능이 추가된 것이다. 둘 다 패키지를 보면 스프링 프레임워크에 의존하고 있다.
그래서 스프링 프레임워크에 의존하지 않는 자바표준(JSR-330)을 사용하는 방법이 있다.
Provider<T> 라는 인터페이스 인데 javax.inject 패키지에 있는 인터페이스 이다. 이 인터페이스를 사용하려면 build.gradle에 라이브러리를 추가해주어야 한다. ( implementation 'javax.inject:javax.inject:1' )
get() 메서드 하나로 기능이 매우 단순하며 스프링 프레임워크에 의존하지 않는다는 장점이 있다.
웹 스코프
웹 스코프들은 웹 환경에서만 동작한다. 프로토 타입과는 다르게 스프링이 해당 스코프의 종료시점까지 관리를 해준다. 따라서 종료메서드가 호출이 된다.
"request" : HTTP 요청하나가 들어오고 나갈때 까지 유지되는 스코프, 각각의 HTTP요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
"session" : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프, HTTP Session과 동일한 생명주기를 가지는 스코프.
"application" : 웹 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프.
"websocket" : 웹 소켓과 동일한 생명주기를 가지는 스코프.
이중에서 request 스코프에만 대해서 예제를 통해서 자세히 알아보자. 나머지 스코프도 범위만 다르지 동작하는 메커니즘은 거의 비슷하다.
클라이언트A와 클라이언트B가 각각 다른 HTTP request를 요청하였다. 컨트롤러에서 myLogger를 요청하는데 myLogger는 request 스코프이다. 이때 클라이언트A 전용 빈, 클라이언트B 전용 빈이 각각 생성되게 되는 것이다.
로그를 출력하기 위한 MyLogger 클래스이다. @Scope(value = "request") 애노테이션을 지정하여 스코프를 request로 설정하였다. 이제 이 빈은 HTTP요청당 한개씩 생성되고, HTTP요청이 끝나는 시점에 소멸된다.
@PostContruct 초기화 메서드를 사용하여 빈이 생성되고 자동주입이 끝난 직후에 uuid를 랜덤하게 지정하였다. java.util.UUID의 uuid를 사용하였다. randoUUID().toString()하면 고유한 uuid값을 만들어준다.
url은 이 빈이 생성되는 시점에는 알 수 없으므로 setter를 이용해서 외부에서 주입받는다.
Service이다.
@RequiredArgsContructor 롬복 애노테이션을 이용하여 myLogger를 생성자 파라메터로 자동주입한다.
logic메서드를 만들어서 MyLogger클래스의 log메서드를 호출해준다.
Controller이다.
@RequiredArgsContructor 롬복 애노테이션을 이용하여 myLoggerService와 myLogger를 생성자 파라메터로 자동주입한다. HTTP요청이 들어오면 그 URL을 25번줄의 url변수에 담아 myLogger에 set해주고, myLogger의 log메서드와 myLoggerService의 logic메서드를 각각 호출해본다.
(참고로 requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다)
오류가 발생하였다.
이유는 스프링 어플리케이션을 실행하는 시점에 싱글톤 빈은 생성하여 주입이 가능하지만, request스코프인 빈은 HTTP요청이 들어와야 생성되기 때문에 아직 생성되지 않았다. 아직 클라이언트의 요청이 들어오지 않았는데 컨트롤러의 저 메서드를 타게 되면 request스코프인 myLogger를 호출하게 되어 오류가 발생하는 것이다.
2가지 해결방법에 대해서 알아보자.
1. Provider의 활용 (ObjectFactory, ObjectProvider 도 마찬가지)
Provider덕분에 myLoggerProvider.get()을 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
myLoggerProvider.get()을 호출하는 시점에는 HTTP요청이 진행중이므로 request scope빈의 생성이 정상 처리된다.
2. 프록시 방식
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
위와 같이 request스코프를 지정해줄 때, proxyMode를 TARGET_CLASS로 지정해주는 방법이 있다.
적용대상이 클래스이면 TARGET_CLASS, 인터페이스이면 TARGET_INTERFACES를 써주면 된다.
이렇게 옵션을 지정하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
스프링 컨테이너는 CGLIB라는 바이트 코드를 조작하는 라이브러리를 사용하여 MyLogger를 상속받은 가짜 프록시 객체를 생성한다. 의존관계 주입에서도 이 가짜 프록시 객체가 주입이 된다.
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 가짜프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다. 클라이언트가 myLogger.logic()을 호출하면 사실은 가짜프록시 객체의 메서드를 호출한 것이다. 가짜프록시 객체는 request 스코프의 진짜 myLogger.logic()를 호출한다.
가짜프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다. (다형성)
이 CGLIB라이브러리는 스프링 빈이 자동으로 싱글톤으로 유지될 수 있도록 할 때에도 사용되었었다. (이전 포스팅 참고)
특징 정리
-> 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
사실 Provider를사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다. 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다. 꼭 웹스코프가 아니어도 프록시는사용할수있다.
주의점
-> 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다. 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지보수 하기 어려워진다.
'JAVA > Spring' 카테고리의 다른 글
빈 생명주기 콜백 (0) | 2021.12.18 |
---|---|
의존관계 자동주입 (0) | 2021.12.17 |
컴포넌트 스캔 (@ComponentScan) (0) | 2021.12.17 |
싱글톤 패턴과 스프링 컨테이너 (0) | 2021.12.15 |
Spring Socket에 대해 알아보자! - 2. WebSocket 만들어보자 (0) | 2020.06.23 |