Basic#8 Bean Scope

17828 단어 SpringSpring

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을 위한 방법으로 ObjectProviderObjectFactory를 제공한다.

  • 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 최초 작성

좋은 웹페이지 즐겨찾기