지금까지 사용한 스프링 빈은 모두 싱글톤 스코프로 생성된 빈이다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
@Scope("prototype")
@Component
public class HelloBean {}
@Scope("prototype")
@Bean
public HelloBean helloBean {return new HelloBean();}
@PreDestroy
같은 소멸 메서드가 호출되지 않는다.public class SingletonTest {
@Test
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");
}
}
}
SingletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@146dfe6
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@146dfe6
[main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@d5b810e
SingletonBean.destroy
public class PrototypeTest {
@Test
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);
Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy(){
System.out.println("SingletonBean.destroy");
}
}
}
find prototypeBean1
SingletonBean.init
find prototypeBean2
SingletonBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@7c8c9a05
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@d41f816
[main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@7c24b813
@PreDestroy
같은 종료 메서드가 전혀 실행되지 않는다.prototypeBean1.destroy();
prototypeBean2.destroy();
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.
결과적으로 프로토타입 빈(x01)의 count는 1이 된다.
결과적으로 프로토타입 빈(x02)의 count는 1이 된다
@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");
}
}
clientBean
이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예를 보자.
clientBean
은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.clientBean
은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.clientBean
에 반환한다.clientBean
은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관한다.)clientBean
을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean
이 반환된다.clientBean.logic()
을 호출한다.clientBean
은 prototypeBean
의 addCount()
를 호출해서 프로토타입 빈의 count를 증가한다.clientBean
을 스프링 컨테이너에 요청해서 받는다.clientBean
이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. clientBean.logic()
을 호출한다.clientBean
은 prototypeBean
의 addCount()
를 호출해서 프로토타입 빈의 count
를 증가한다.count
값이 1이었으므로 2가 된다.@Scope("singleton")
static class ClientBean{
private final PrototypeBean prototypeBean;
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic(){
prototypeBean.addCount();
return prototypeBean.count;
}
}
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
...
}
ac.getBean()
을 통해 logic()
을 호출하면 항상 새로운 프로토타입 빈이 생성된다.ApplicationContext
전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.ObjectProvider
이다ObjectFactory
에 편의 기능을 추가한 것이 ObjectProvider
이다.static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
...
}
ObjectProvider
의 타입을 PrototypeBean
클래스로 지정하게 되면, getObject()
를 통해 스프링 컨테이너에서 해당 빈을 찾아서 반환한다. (DL)import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.ObjectFactory;
ObjectProvider
, ObjectFactory
는 모두 스프링에 의존적이다.javax.inject.Provider
는 JSR-330 자바 표준을 사용하는 방법이다.static class ClientBean {
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
...
}
Provider
의 타입을 PrototypeBean
클래스로 지정하게 되면, get()
을 통해 스프링 컨테이너에서 해당 빈을 찾아서 반환한다. (DL)ObjectProvider
, JSR330 Provider
등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
@Predestroy
)가 호출된다.request
: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.session
: HTTP Session과 동일한 생명주기를 가지는 스코프application
: 서블릿 컨텍스트(ServletContext
)와 동일한 생명주기를 가지는 스코프websocket
: 웹 소켓과 동일한 생명주기를 가지는 스코프동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이럴때 사용하기 좋은것이 바로 request 스코프다.
@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(){
uuid = UUID.randomUUID().toString();
System.out.println("["+uuid+"] request scope bean created: "+this);
}
@PreDestroy
public void close(){
System.out.println("["+uuid+"] request scope bean close: "+this);
}
}
@Scope
설정을 request
로 하였으므로, HTTP요청 하나당 하나의 빈이 생성되며, 요청이 끝나는 시점에 소멸된다.@PostConstruct
를 통해 빈 등록이 끝나면 UUID를 설정한 후, 빈이 생성되었다는 로그를 남긴다.@Predestroy
를 통해 빈의 소멸 직전에 소멸되었다는 로그를 남긴다.@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";
}
}
log-demo
라는 경로로 접속하면, 해당 컨트롤러가 매핑되며 log-demo
메소드가 호출된다.getRequestURL()
을 통해 접속한 URL정보를 저장한다.@Responsebody
로 문자를 반환한다.MyLogger
는 HTTP요청마다 하나씩 생성되므로, 섞일일이 없다.@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
LogDemoController
에서 호출하며 전달한 ID를 로그로 찍는다.MyLogger
덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger
의 멤버변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
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;
LogDemoService
클래스와 LogDemoController
클래스는 싱글톤 빈으로 컨테이너에 등록된다.LogDemoController
가 의존관계 주입을 위해 MyLogger
를 요청하지만, MyLogger
는 request scope으로 아직 생성되지 않았다.ScopeNotActiveException
오류가 발생한다.ObjectProvider는 지정한 빈을 getObject()를 호출한 시점에 스프링 컨테이너에 요청해서 제공해주는 기능이다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
...
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);
...
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
MyLogger
에 대한 의존관계 주입을 바로 받지 않고, ObjectProvider
를 통해 ObjectProvider.getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연하였다.ObjectProvider.getObject()
를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.[bf5723cc-5b03-4159-9028-02755f80a027] request scope bean created: hello.core.common.MyLogger@3beda921
[bf5723cc-5b03-4159-9028-02755f80a027][http://localhost:8080/log-demo] controller test
[bf5723cc-5b03-4159-9028-02755f80a027][http://localhost:8080/log-demo] service id = testId
[bf5723cc-5b03-4159-9028-02755f80a027] request scope bean close: hello.core.common.MyLogger@3beda921
ObjectProvider.getObject()
를 LogDemoController
, LogDemoService
에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈(MyLogger@3beda921
)이 반환된다.LogDemoController
, LogDemoService
클래스는 Provider
사용 이전의 코드와 동일하다.@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {...}
@Scope
의 속성에 proxyMode = ScopedProxyMode.TARGET_CLASS
를 추가하면된다.TARGET_CLASS
를 선택INTERFACES
를 선택MyLogger
의 클래스를 확인해보면, 순수 MyLogger
클래스가 아닌, MyLogger$$EnhancerBySpringCGLIB
이라는 객체가 대신 등록되어있다.MyLogger
를 상속받은 가짜 프록시 객체를 생성하여 컨테이너에 등록한다.myLogger
이지만, MyLogger$$EnhancerBySpringCGLIB
객체인 것이다.myLogger
를 찾는 방법을 알고 있다.myLogger.logic()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.myLogger.logic()
를 호출한다.Provider
의 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.출처: 인프런 스프링 핵심 원리 - 기본편 (김영한)
인프런 스프링 핵심 원리