@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
@PreDestory
같은 종료 메소드가 호출되지 않는다. 따라서 종료 메소드에 대한 호출도 클라이언트가 직접 해야한다.public class SingletonTest {
@Test
public void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close(); //종료
}
@Scope("singleton")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
public class PrototypeTest {
@Test
public void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close(); //종료
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
@PreDestory
같은 종료 메소드가 실행되지 않는다.clientBean.logic()
을 호출하고 clientBean에서 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. count 값은 1이 된다.clientBean.logic()
을 호출하면, clientBean에서 addCount()를 호출한다. 이 때 새로 생성된 프로토타입 빈이 아니라 이미 생성되었던 빈을 그대로 사용하므로 원래 count 값이 1이였으므로 2로 증가된다.public class SingletonWithPrototypeTest1 {
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(2);
}
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.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
✔️ 정리하면, 싱글톤 빈과 프로토타입 빈을 함께 사용하면, 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입도 생성된 시점에 주입이 된 이후에 새로 생성되지 않고 계속 유지되는 것이 문제다.
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
//logic()이 실행될 때마다, 스프링 컨테이너에서 새로운 PrototypeBean을 요청하여 생성한다.
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
prototypeBeanProvider.getObject()
를 통해서 항상 새로운 프로토타입 빈이 생성된다.ObjectFactory
: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존ObjectProvider
: ObjectFactory 상속, 옵션 스트림 처리 등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존javax.inject.Provider
라는 JSR-330 자바 표준을 사용하는 방법이다.jakarta.inject.Provider
를 사용한다.javax.inject:javax.inject:1
jakarta.inject:jakarta.inject-api:2.0.1
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
HTTP Session
과 동일한 생명주기를 가지는 스코프ServletContext
와 동일한 생명주기를 가지는 스코프🔼 위의 각 종류들은 범위만 다르고 동작 방식은 비슷하다. request를 예로 정리한다.
✔️ 참고 : 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext
을 기반으로 어플리케이션을 구동한다. 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext
를 기반으로 어플리케이션을 구동한다.
@Component
//Scope를 request로 지정
@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() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
request
를 사용해서 이 빈은 HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸된다.requestURL
은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
🔼 MyLogger가 잘 동작하는지 테스트하기 위한 컨트롤러이다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service Id = " + id);
}
}
🔼 서비스 계층에서도 로그를 기록하기 위해 작성한 서비스이다.
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;
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
//private final MyLogger myLogger;
private final ObjectProvider<MyLogger> myLoggerProvider; //Provider를 주입받는다.
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
//Provider를 통해서 등록된 MyLogger 빈을 찾아서 넣어준다.
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
ObjectProvider.getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연(정확히는 스프링 컨테이너에게 빈 생성 요청하는 시기)할 수 있다.ObjectProvider.getObject()
을 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈이 정상적으로 생성된다.@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
proxyMode = ScopedProxyMode.TARGET_CLASS
와 같이 Scope에 적용할 수 있다.TARGET_CLASS
, 인터페이스면 INTERFACES
를 사용하면 된다.myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
참고 Reference