빈 스코프는 말 그대로 빈이 존재할 수 있는 범위를 뜻한다. 스프링은 다음과 같은 다양한 스코프를 지원한다.
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 빈을 반환한다.
프로토타입 스코프의 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
프로토타입의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 프로토타입의 빈을 생성하고, 필요한 의존관계를 주입한다.
싱글톤 빈은 컨테이너 생성 시점에 같이 생성되고 초기화
되지만, 프로토타입 빈은 스프링 컨테이너에서 빈을 조회할 때 생성
되고 초기화 메서드도 실행
된다.
class PrototypeTest {
@Test
void prototypeBeanFind() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
final PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean2");
final PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
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");
}
}
}
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다. 클라이언트에게 빈을 반환한 이후에는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 클라이언트에게 있다. 따라서 @PreDestory와 같은 종료 콜백 메서드가 호출되지 않는다.
프로토타입 스코프의 빈은 해당 타입의 빈이 스프링 컨테이너에 요청될 때마다 생성된다. 그런데 싱글톤 스코프의 빈이 프로토타입의 빈을 주입 받는다면, 싱글톤 스코프의 빈이 생성되고 의존 관계가 주입되는 시점에만 프로토타입 빈이 조회될 것이고, 이후에는 계속 같은 빈이 사용될 것이다. 대부분의 경우, 개발자는 이를 의도하지 않았을 것이다. 애초에 매번 새로운 객체가 필요하기 때문에 프로토 타입으로 선언했을 것이기 때문이다.
따라서 아래와 같은 테스트 코드를 실행한다면, 테스트는 실패할 것이다.
class SingletonWithPrototypeTest1 {
@Test
void singletonClientUserPrototype() {
final AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
final ClientBean clientBean1 = ac.getBean(ClientBean.class);
final int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
final ClientBean clientBean2 = ac.getBean(ClientBean.class);
final int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
ac.close();
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean; // 생성 시점에 주입
public ClientBean(final PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@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");
}
}
}
가장 간단한 방법은, 프로토타입의 의존 관계를 주입받지 않고 ApplicationContext에 매번 빈을 아래와 같이 요청하는 것이다. 하지만 이렇게 되면, 스프링에 종속적인 코드가 되고 단위테스트도 어려워 진다.
여기서 말하는 DL(Dependency Lookup)
의존관계를 외부에서 주입받는 걸 Dependency Injection(DI)이라 불렀다면, 직접 필요한 의존관계를 찾는 것은 Dependency Lookup(DL)이라고 부른다. ObjectFactory와 ObjectProvider가 스프링에서 DL 기능을 제공한다.
ObjectFactory, ObjectProvider
ObjectProvider: 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다.
ObjectFactory: ObjectProvider의 구 버전 클래스이다.
스프링 컨테이너에서만 사용 가능하다는 단점이 있다.
@Scope("singleton")
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
final PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
1) ObjectFactory, ObjectProvider
ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup) 서비스를 제공한다. 원래는 ObjectFactory만 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다. 아래와 같이 사용할 수 있다.
@Scope("singleton")
static class ClientBean {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
public ClientBean(final ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
final PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
특징
1) 스프링이 제공하는 기능을 사용하긴 하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.
2) 딱 필요한 DL(Dependency Lookup) 기능만 제공한다.
2) JSR-330 Provider
이 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다. 이 방법을 사용하기 위해선 javax.inject:javax.inject:1 라이브러리를 gradle에 별도로 추가해야 한다. 아래와 같이 사용할 수 있다.
@Scope("singleton")
static class ClientBean {
private final Provider<PrototypeBean> prototypeBeanProvider;
ClientBean(final Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
final PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
특징
1) 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
2) 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.
3) 딱 필요한 DL(Dependency Lookup) 기능만 제공한다.
4) 별도의 라이브러리가 필요하다.