Basic#8 Bean Scope
8. Bean Scope
1. Bean Scope?
Bean의 Lifecycle을 의미한다.
- Singleton
- Prototype
- Web 관련 scope
- request
- session
- application
Bean의 default lifecycle은 singleton이다. 이외에 다른 scope를 지정하고 싶다면 @Scope()
annotation을 이용한다.
2. Prototype scope
Singleton은 항상 같은 instance를 반환한다면, prototype은 매번 새로운 instance를 생성해서 반환한다.
Singleton
Container 생성 시점에 초기화 method가 실행되고 bean을 여러번 조회해도 같은 instance를 참조한다. 또한 container가 종료될 때 bean의 종료 method가 실행된다.
Prototype
Container 생성 시점이 아닌, Bean을 조회하는 시점에 생성되고 초기화된다. 조회할 때 bean이 생성되기 때문에 각각 다른 instance가 생성된다. 또한 생성+DI까지만 관여하고 이후로는 관리하지 않기 때문에 container가 종료되어도 bean의 종료 method가 실행되지 않는다. 특정 bean의 종료 method가 필요할 경우에는 직접 호출해야 한다.
@Scope("prototype")
static class PrototypeBean {
@PostConstruc
public void init() {
//init
}
@PreDestroy
public void close() {
//close
}
}
AnnotationConfigApplicationContext ac = new AnnoatationConfigApplicationContext(PrototypeBean.class);
PrototypeBean bean1 = ac.getBean(PrototypeBean.class); // 이 시점에 생성+DI
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
ac.close(); // PrototypeBean의 종료 method는 실행되지 않는다
3. Singleton과 함께 사용 시 문제점
Singleton bean이 prototype bean을 주입받아 사용할 때 문제가 발생한다. Singleton은 container 생성 시점에 bean을 생성하고 DI를 수행한다. 즉, field로 prototype을 가지고 있으면 해당 prototype bean도 그 시점에 생성해서 들고 있기 때문에 사실상 singleton과 다른게 없다.
이로 인해서 매번 새롭게 생성되서 사용되어야 할 bean이지만 그렇지 않기 때문에 문제가 발생한다. 생명 주기도 singleton bean과 함께하기에 차라리 singleton으로 지정하거나 그런 의도가 없었다면 다른 방법을 통해 prototype을 사용해야 한다.
4. 그럼 어떻게 항상 새로운 prototype bean을 요청할 수 있을까?
무식한 방법으로는 ApplicationContext
를 field로 가지며 이를 통해서 prototype bean을 생성하고 가져오는 방법이다. 물론 이렇게 사용하는 것은 바람직하지 않다.
Spring은 이러한 문제를 해결하기 위해 DL 기능을 제공한다. 외부에서 DI를 하는 것이 아니라 직접 필요한 관계를 찾는 것을 Dependency Lookup, DL이라고 하며 prototype bean을 요청할 때 필요한 기능이다.
5. DL (Dependency Lookup)
ObjectProvider, ObjectFactory
앞서 ApplicationContext
를 가지고 prototype bean을 조회했지만 spring은 DL을 위한 방법으로 ObjectProvider
와 ObjectFactory
를 제공한다.
- ObjectProvider
container에 있는 prototype bean을 찾아서 반환해준다. DL과 관련된 여러 편의 기능도 포함된다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public void logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
...
}
- ObjectFactory
ObjectProvider의 상위 클래스로 기본 기능만을 갖추고 있다. 위의 예제 코드에서 ObjectProvider를 ObjectFactory로 변경해도 바로 동작이 가능하다.
SSR-330 Provider
Java에서 제공하는 DL 기능이다. 딱 DL만을 제공하기 때문에 간단하며 자바 표준으로 spring framework에 의존하지 않는다.
하지만, ObjectProvider가 스트림처리와 같은 편의 기능도 제공하고 있고 framework를 변경하며 처리할 일이 별로 없기 때문에 대부분 ObjectProvider를 이용한다.
6. Web Scope
Web scope는 Web 환경에서만 동작한다. Prototype과는 다르게 종료시점까지 관리하기 때문에 종료 method는 실행된다.
- request
HTTP 요청이 하나 들어오고 나갈 때까지 유지되는 scope. (각각의 요청마다 bean instance가 생성되고 관리된다.) - session
- websocket
request scope 예제
Log를 찍는 bean을 만들고 request가 들어오면 해당 bean이 동작하는 예제를 구현해본다.
Log의 format은 [UUID][requestURL]{message}
로 정한다.
MyLogger.java
@Component
@Scope("request")
public class MyLogger {
private String uuid;
private String requestURL;
// requestURL은 사용자가 HTTP 요청을 보내야만 알 수 있다. 즉 생성 시점에 지정할 수 없다.
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");
}
@PreDestroy
public void close() {
System.out.println("["+uuid+"] request scope bean close");
}
}
Web에서 사용자가 HTTP 요청을 보내기 전까지는 requestURL
을 알 수 없기 때문에 이는 setter로 설정한다. 반면 uuid
는 유일한 값이며 해당 request에 대한 id이다. 따라서 초기화 method에서 관리한다.
LogDemoController.java
@Controller
@RequiredArgConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger; // error
@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";
}
}
위 code에서 error가 발생한다. 그 이유는 lifecycle 때문인데, MyLogger
는 request scope를 갖는다. 즉, request가 들어온 시점부터 응답으로 반환될 때까지가 life cycle이지만 LogDemoController
가 이를 field로 지니고 있어서 container가 실행되고 LogDemoController
bean이 생성될 때 호출되는 문제를 가지고 있다.
여기서 우리는 앞서 배웠던 Provider를 이용해서 DL을 통해 이 문제를 해결할 수 있다.
LogDemoController.java
...
private final ObjectProvider<MyLogger> myLoggerProvider;
...
public String logDemo(HttpServletRequest request) {
...
MyLogger myLogger = myLoggerProvider.getObject();
...
}
Proxy
Provider를 이용하는 방법 외에 Proxy를 이용할 수 있다. error가 발생했던 당시의 코드로 돌리고 단순히 MyLogger
의 scope에 추가적으로 parameter만 입력하면 된다.
MyLogger.java
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
CGLIB 라는 byte code를 조작하는 library를 통해서 MyLogger
를 상속받는 가짜 proxy 객체를 생성한다. spring container는 이 가짜 proxy 객체를 bean으로 등록하고 내부에서 진짜로 해당 객체를 참조할 땐 가짜 proxy 객체가 진짜 객체를 반환해주는 방법으로 동작한다.
가짜 proxy 객체는 단순히 위임 logic을 갖고 있기 때문에 실체 객체를 찾아 반환해준다. container 입장에서는 가짜 proxy 객체의 내부적인 요소보다 가짜 proxy 객체 자체만을 바라보기 때문에(다형성) singleton처럼 동작할 수 있다.
중요한 점은 provider든, proxy든 객체 조회가 실제로 발생할 때까지 지연시키는 방법으로 이러한 scope를 처리한다는 것이다.
이 글을 끝으로 김영한님의 스프링 핵심 원리 - 기본편 정리를 마무리합니다.
🛠 계속 업데이트 필요!
2022.04.16 최초 작성
Author And Source
이 문제에 관하여(Basic#8 Bean Scope), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dev2danis/Spring-Basic8-Bean-Scope저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)