인프런 김영한님의 스프링 핵심 원리 - 기본편 강의 내용을 바탕으로 작성한 글입니다.
지금까지 다양한 빈 스코프에 대해 알아보았다. 개발자는 어플리케이션의 특성에 따라 적절한 빈 스코프를 선택해야 하지만, 프로토타입 빈과 싱글톤 빈을 함께 사용할 때는 주의해야 할 점이 있다.
먼저 아래와 같이 동작하는 프로토타입 빈이 있다고 가정하자.
PrototypeBean@x01
)를 생성해서 반환한다.PrototypeBean@x01.count
: 0addCount()
를 호출하여 count 필드를 1 증가시킨다. PrototypeBean@x01.count
: 1위와 같은 상황에서, 클라이언트 B가 등장했다고 생각해보자.
PrototypeBean@x02
)를 생성해서 반환한다.PrototypeBean@x02.count
: 0addCount()
를 호출하여 count 필드를 1 증가시킨다.PrototypeBean@x02.count
: 1테스트 코드를 통해 확인해보자.
ExampleBean
public class ExampleBean {
private int count = 0;
public ExampleBean() {
System.out.println("create ExampleBean");
}
@PostConstruct
public void init() throws Exception {
System.out.println("ExampleBean.init");
}
@PreDestroy
public void close() throws Exception {
System.out.println("ExampleBean.destroy");
}
public void addCount() {
count++;
}
public int getCount() {
return count;
}
}
ExampleTest
public class ExampleTest {
@Test
public void exampleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(ExampleConfig.class);
System.out.println("ac = " + ac);
// exampleBean1
ExampleBean exampleBean1 = ac.getBean(ExampleBean.class);
exampleBean1.addCount();
System.out.println("exampleBean1.count = " + exampleBean1.getCount());
// exampleBean2
ExampleBean exampleBean2 = ac.getBean(ExampleBean.class);
exampleBean2.addCount();
System.out.println("exampleBean2.count = " + exampleBean2.getCount());
}
@Configuration
static class ExampleConfig {
@Bean
@Scope("prototype")
public ExampleBean exampleBean() {
return new ExampleBean();
}
}
}
실행 결과
count값이 예상대로 증가하는 것을 확인할 수 있다. 이처럼 프로토타입 빈을 직접 요청할 때는 문제가 없지만, 싱글톤 빈에서 프로토타입 빈을 사용할 때는 문제가 발생한다.
singletonBean
의 인스턴스가 생성되고 의존관계 자동 주입이 발생한다.singletonBean
은 의존관계를 주입받기 위해 스프링 컨테이너에 프로토타입 빈을 요청한다.PrototypeBean@x02
)를 생성하여 싱글톤 빈에게 참조값을 반환해준다.PrototypeBean@x02.count
: 0singletonBean
은 싱글톤 빈이기 때문에, 주입받은 프로토타입 빈의 참조값 PrototypeBean@x02
는 이 시점 이후로 변경되지 않는다.singletonBean@x01
을 반환해준다.singletonBean.logic()
을 호출하면, 싱글톤 빈은 prototypeBean.addCount()
를 호출하여 count를 1 증가시킨다.PrototypeBean@x02.count
: 1singletonBean@x01
을 반환해준다.singletonBean.logic()
을 호출하면, 싱글톤 빈은 prototypeBean.addCount()
를 호출하여 count를 1 증가시킨다.PrototypeBean@x02.count
: 2코드를 통해 직접 확인해보자.
public class ExampleTest {
@Test
public void exampleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class, PrototypeBean.class);
// 클라이언트 A
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
System.out.println("client A call logic()");
singletonBean1.logic();
// 클라이언트 B
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("client B call logic()");
singletonBean2.logic();
}
@Scope("singleton")
static class SingletonBean {
private PrototypeBean prototypeBean;
@Autowired
public SingletonBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public void logic() {
prototypeBean.addCount();
System.out.println("count = " + prototypeBean.getCount());
}
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public int getCount() {
return count;
}
public void addCount() {
count++;
}
}
}
count값이 1이 되어야 하는 예상과 달리 count값이 2가 된 걸 확인할 수 있다. 싱글톤 빈이 생성 시점에만 의존관계를 주입받기 때문에 싱글톤 빈에 주입된 프로토타입 빈도 싱글톤 빈과 함께 계속 살아있어서 이런 일이 발생한다.
프로토타입 빈은 스프링 컨테이너에 요청될 때마다 생성되므로, 프로토타입 빈을 사용할 때마다 싱글톤 빈에서 ac.getBean(PrototypeBean.class)
를 통해 프로토타입 빈을 요청하면 해결되기는 한다. 하지만 이런 방법은 빈이 ApplicationContext를 받아와야 하기 때문에, 스프링에 종속적이게 되고 단위 테스트가 어려워진다.
싱글톤 빈은 프로토타입 빈을 사용하려 할 때마다 스프링 컨테이너에 의존관계를 요청해야 한다. 이처럼 의존관계를 주입받지 않고, 필요할 때 직접 요청하는 것을 Dependency Lookup (의존관계 조회) 라고 한다.
스프링에서는 ObjectProvider
를 통해 특정 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다. SingletonBean을 아래와 같이 수정한 뒤 테스트를 다시 실행해보자.
@Scope("singleton")
static class SingletonBean {
private ObjectProvider<PrototypeBean> objectProvider;
@Autowired
public SingletonBean(ObjectProvider<PrototypeBean> objectProvider) {
this.objectProvider = objectProvider;
}
public void logic() {
PrototypeBean prototypeBean = objectProvider.getObject();
prototypeBean.addCount();
System.out.println("count = " + prototypeBean.getCount());
}
}
count값이 1이 된 것을 확인할 수 있다. 의존관계 주입 시점에 ObjectProvider를 대신 주입받고, 프로토타입 빈이 필요한 시점에 objectProvider.getObject()
를 통해 프로토타입 빈을 요청하여 새로 생성된 인스턴스를 받아오기 때문이다.
ObjectProvider는 스프링에 의존적이다. 스프링에 의존하고 싶지 않다면, javax.inject.Provider
를 사용하면 된다. (gradle 추가 설정 필요)
@Scope("singleton")
static class SingletonBean {
private Provider<PrototypeBean> provider;
@Autowired
public SingletonBean(Provider<PrototypeBean> provider) {
this.provider = provider;
}
public void logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
System.out.println("count = " + prototypeBean.getCount());
}
}