- 해당 게시물은 인프런 "스프링 핵심 원리 - 기본편" 강의를 참고하여 작성한 글입니다.
- 자세한 코드 및 내용은 강의를 참고해 주시길 바랍니다.
강의링크 -> 스프링 핵심 원리 - 기본편 (김영한)
빈 스코프란 빈이 존재할 수 있는 범위를 뜻한다. 지금까지는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다고 학습했는데 이는 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스프링은 다양한 스코프를 지원한다
@Scope("prototype")
@Component
public class HelloBean {}
빈 스코프는 다음과 같이 지정할 수 있다.
스프링 컨테이너가 항상 같은 인스턴스의 스프링 빈을 반환하는 싱글톤 스코프와 다르게 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
1) 포로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2) 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
3) 스프링 컨테이너느 생성한 프로토타입 빈을 크라이언트에 반환한다.
4) 이후 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로로타입 빈을 생성해서 반환한다.
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리하기 때문에 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
=> @PreDestroy 같은 종료 메서드가 호출되지 않는다.
public class SingletonWithPrototypeTest1 {
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class); // ClientBean과 PrototypeBean은 컴포넌트 스캔으로 자동 등록
ClientBean clientBean1 = ac.getBean(ClientBean.class);
...
ClientBean clientBean2 = ac.getBean(ClientBean.class);
...
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
...
}
}
@Scope("prototype")
static class PrototypeBean {
...
}
}
싱글톤 빈인 ClientBean
은 new AnnotationConfigApplicationContext()으로 생성시점에 생성자를 통해 의존관계 주입을 받았다. 사용자는 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때마다 새로 생성해서 사용하길 원할 것이다. 그런데, 위의 경우에는 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되게 되는데 이는 Provider로 문제를 해결할 수 있다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
ObjectProvider
의 getObject()
를 호출하면 그 때서야 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.JSR-330 Provider
를 사용해도 괜찮다.@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
get()
메서드 하나로 기능이 매우 단순하다.동시에 여러 HTTP 요청이 오면 정확힌 어떤 요청이 남긴 로그인지 구분하기 어려운데 이럴 때 request 스코프를 사용하면 구분을 쉽게 할 수 있다.
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
...
@PostConstruct
public void init() {
...
}
@PreDestroy
public void close() {
...
}
}
로그 출력을 위한 MyLogger
클래스이다. request
스코프로 지정했고 해당 빈은 HTTP 요청 당 하나 씩 생성되고, 요청이 끝나는 시점에 소멸된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
...
}
}
로거가 잘 작동하는지 확인하기위한 테스트용 컨트롤러인데 코드를 실행하면 Scope 'request' is not active for the current thread;
라고 에러가 뜬다. 이는 LogDemoController
가 의존성 주입을 받을 때 스프링 컨테이너에게 MyLogger
를 요청하는데 MyLogger
는 아직 생성되지 않았기 때문에 발생하는 에러이다. request
스코프 빈은 HTTP 요청 하나가 들어와야 생성되기 때문이다.
이는 앞에서 배운 Provider
를 통해 해결할 수 있다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerProvider.getObject();
...
}
}
ObjectProvider
의 getObject()
를 호출하는 시점까지 reuest
스코프 빈의 생성을 지연할 수 있다. ObjectProvider.getObject()
를 호출하는 시점에는 HTTP 요청이 진행중이므로 request
스코프 빈의 생성이 정상 처리된다.
그런데 이것보다 더 간단한 방법이 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
프록시를 사용하면 LogDemoController
의 코드를 바꾸지 않아도 된다. @Scope
의 proxyMode = ScopedProxyMode.TARGET_CLASS
를 설정하면 스프링 컨테이너는 CGLIB
라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger
를 상속받는 가짜 프록시 객체를 생성한다. 가짜 프록시를 주입하고 실제 요청이 오면 내부에 들이어있는 실제 빈을 요청하는 위임 로직을 이용하여 실제 메서드를 호출한다. 중요한 것은 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 점이다.