Bean Scope?
Scope는 번역 그대로 Bean이 존재할 수 있는 범위를 뜻한다.
Spring은 다음과 같은 다양한 Scope를 지원한다
|Scope|Description|
|–|–|
|Singleton|기본스코프, 스프링 컨테이너의 시작-종료까지 유지되는 가장 넓은 범위의 스코프|
|Prototype|스프링컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프|
|request
(web)|web 요청이 들어오고 나갈 때까지 유지되는 스코프|
|session
(web)|Web 세션이 생성되고 종료될 때까지 유지되는 스코프|
|application
(web)|웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프|
Compnent Scan 시
@Scope("prototype")
@Compnent
public class HelloBean{}
수동 등록 시
@Scope("prototype")
@Bean
PrototypeBean HelloBean(){
return new HelloBean();
}
Singleton Scope
1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청
2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환
3. 이후 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈 반환
public class SingletonTest {
@Test
void singleTonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean bean1 = ac.getBean(SingletonBean.class);
SingletonBean bean2 = ac.getBean(SingletonBean.class);
System.out.println("bean1 = " + bean1);
System.out.println("bean2 = " + bean2);
Assertions.assertThat(bean1).isSameAs(bean2);
ac.close();
}
@Scope("singleton") //("singleton") 생략가능
static class SingletonBean{
@PostConstruct
public void init(){
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("SingletonBean.destroy");
}
}
}
Prototype Scope
1. 프로토 타입 스코프의 빈을 스프링 컨테이너에 요청
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계에 주입하여 빈을 반환.
3. 이후에 같은 ㅛ청이 와도 항상 새로운 프로토타입 빈을 생성해 반환
핵심
Spring Container는 prototype Bean 을 생성하고, 의존관계 주입, 초기화까지만처리한다는 것이다.
클라이언트에 Bean을 반환하고, 이후 Spring Container는 생성된 prototype Bean을 관리하지 않는다. 즉, prototype Bean을 관리할 책임은 prototype Bean을 받은 Client에 있다.
그러므로 @PreDestroy
같은 종료 메서드가 호출되지 않는다.
public class PrototypeTest {
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
System.out.println("bean1 = " + bean1);
System.out.println("bean2 = " + bean2);
Assertions.assertThat(bean1).isNotSameAs(bean2);
ac.close();
}
@Scope("prototype")
@Component
static class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("prototypeBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("prototypeBean.destroy");
}
}
}
테스트 결과
find prototypeBean1 // 두 개 빈 조회될때마다 따로 생성
prototypeBean.init
find prototypeBean2 // 두 개 빈 조회될때마다 따로 생성
prototypeBean.init
bean1 = hello.core.scope.PrototypeTest$PrototypeBean@3f4faf53 // 인스턴스 다름
bean2 = hello.core.scope.PrototypeTest$PrototypeBean@7fd50002 // 인스턴스 다름
//close되지 않음. -> @preDestroy 실행 안되었음을 확인할 수 있음
23:17:14.257 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@69b2283a, started on Sat May 13 23:17:14 KST 2023
Prototype, Singleton Scope를 같이 쓸때 문제점
하지만 Singleton Bean과 함께 사용할때에는 의도한 대로 잘 동작하지 않으므로 주의가 필요하다
public class SingletonWPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
bean1.addCount();
Assertions.assertThat(bean1.getCount()).isEqualTo(1);
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
bean2.addCount();
Assertions.assertThat(bean2.getCount()).isEqualTo(1);
}
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class,PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
Assertions.assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
Assertions.assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
static class ClientBean{
//생성시점에 주입 되어 계속 같은걸 씀.
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean{
private int count = 0 ;
public void addCount(){
count++;
}
public int getCount(){
return count;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.inti"+this);
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy");
}
}
}
++참고 해결방법 (prototype Bean을 사용할때마다 생성하기)
Singleton Bean이 prototype Bean을 사용할 때 마다 Spring Container에 새로 요청하기. (원시적인 방법이므로 추천되지 않음)
해결방법(2) (@ObjectProvider 사용하기)
@ObjectFactory
or ObjectProvider
Annotation 사용하기!!
@ObjectProvider
의 핵심 컨셉은 prototype Bean을 대신 조회해 주는 대리인 같은 느낌이다!
다만 Spring에 의존적인 코드가 된다는 단점도 존재한다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public class SingletonWPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
bean1.addCount();
Assertions.assertThat(bean1.getCount()).isEqualTo(1);
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
bean2.addCount();
Assertions.assertThat(bean2.getCount()).isEqualTo(1);
}
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class,PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
Assertions.assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
Assertions.assertThat(count2).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean{
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Scope("prototype")
static class PrototypeBean{
private int count = 0 ;
public void addCount(){
count++;
}
public int getCount(){
return count;
}
@PostConstruct
public void init(){
System.out.println("PrototypeBean.inti"+this);
}
@PreDestroy
public void destroy(){
System.out.println("PrototypeBean.destroy");
}
}
}
/*
PrototypeBean.intihello.core.scope.SingletonWPrototypeTest1$PrototypeBean@6f10d5b6
PrototypeBean.intihello.core.scope.SingletonWPrototypeTest1$PrototypeBean@222a59e6
두 개의 각각 다른 인스턴스가 반환되는 것을 볼 수 있다.
*/
해결방법(3) - Provider 라이브러리 사용하기
별도의 라이브러리가 필요함.
하지만 자바 표준이므로 Spring이 아닌 다른 Container에서도 사용가능!
Dependency
//build.gradle
dependencies {
...
// 라이브러리 추가!
implementation 'javax.inject:javax.inject:1'
...
}
사용예시
@Scope("singleton")
static class ClientBean{
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
Web Scope
프로토타입과는 다르게 Spring이 해당 스코프의 종료시점까지 관리하므로, 종료메서드가 호출된다.
web scope 종류 | - |
---|---|
request (web) |
web 요청이 들어오고 나갈 때까지 유지되는 스코프 각 Http 요청마다 별도의 빈 인스턴스가 생성되고 관리된다. |
session (web) |
Web 세션이 생성되고 종료될 때까지 유지되는 스코프 HTTP Session과 동일한 생명주기를 가진다. |
application (web) |
웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프 서블릿 컨텍스트와 동일한 생명주기를 가진다. |
websocket (web) |
웹소켓과 동일한 생명주기를 가지는 스코프 |
Request Scope
web library 추가
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-web'
...
}
아래와 같이 로그가 남도록 Request Scope를 활용해볼 것이다.
UUID를 이용해 HTTP요청을 구분할 것이고,
requestURL정보도 추가로 넣어 어떤 URL을 요청해서 남은 로그인지 확인해보겠다.
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + " [" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
this.uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean crete:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String testId) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + testId);
}
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controllet test");
logDemoService.logic("testId");
return "ok";
}
}
Log
[5d138431-afac-4ab0-a20c-a9f5b25409be] request scope bean crete:hello.core.common.MyLogger@4b153236
[5d138431-afac-4ab0-a20c-a9f5b25409be] [http://localhost:8080/log-demo] controllet test
[5d138431-afac-4ab0-a20c-a9f5b25409be] [http://localhost:8080/log-demo] service id = testId
[5d138431-afac-4ab0-a20c-a9f5b25409be] request scope bean close:hello.core.common.MyLogger@4b153236
Scope 와 Proxy
ObjectProvider(MyLogger) 와 같은 추가적인 코드작성이 필요하다. 하지만 Proxy를사용하면 위와 같은 코드추가 없이 해결이 가능하다.
가짜 Proxy Class(CGLIB)를 만들어 주입시키는 개념이다.
단 무분별한 사용은 유지보수에 어려움을 주므로 꼭 필요할 때 사용하도록..
@Scope(value = “request”, proxyMode = ScopedProxyMode.TARGET_CLASS)추가
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) //proxyMode = ScopedProxyMode.TARGET_CLASS 추가
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + " [" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
this.uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean crete:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controllet test");
logDemoService.logic("testId");
return "ok";
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String testId) {
myLogger.log("service id = " + testId);
}
}
Log
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] request scope bean crete:hello.core.common.MyLogger@7b7b8f33
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] [http://localhost:8080/log-demo] controllet test
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] [http://localhost:8080/log-demo] service id = testId
[09fcbeae-8fc8-49da-b309-b958e1e63f0b] request scope bean close:hello.core.common.MyLogger@7b7b8f33