스프링의 프로토타입과 싱글톤 빈 같이 사용하기
싱글톤으로 클라이언트의 요청을 받으며, 프로토타입 빈을 주입받아 사용하고 싶을 때 어떻게 해야 할까?
일단 주입을 받아보면 어떻게 될까?
싱글톤 빈에 프로토타입빈을 주입을 받아 보자.
싱글톤 빈은 생성 시점에 생정자 주입을 받았으므로 프로토타입 빈을 새로 생성하지 않게 된다.
이렇게 되면 결국 싱글톤과 다를 바가 없게 된다.
테스트 결과를 살펴보면 count2는 값이 2인 것을 확인할 수 있다.
@Test
void singletonClientUsePrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean1.logic();
assertThat(count2).isEqualTo(2);
}
static class ClientBean {
// 생성 시점에 주입이 되어있기 때문에 다시 요청을 한다고 해도 동일한 빈이 사용된다. 즉, 싱글톤 마냥 사용되는 것이다.
// 하지만 매번 새로운 프로토 타입을 주입 받고 싶은게 목적이다. 어떻게 해야 할까?
private final PrototypeBean prototypeBean;
ClientBean(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;
}
}
어떻게 싱글톤에서 프로토타입 빈을 제대로 주입 받을 수 있을까?
ApplicationContext를 주입 받기
좀 무식한 방법이긴 하지만 ApplicationConetext를 주입 받아 getBean() 하는 방법이다.
해결은 되지만 컨테이너를 주입 받는다.. 라는게 좋은 코드의 느낌은 아니다.
스프링 컨테이너에 완전 종속적이게 되고, 단위 테스트도 어려워지게 된다.
static class ClientBean {
// 생성 시점에 주입이 되어있기 때문에 다시 요청을 한다고 해도 동일한 빈이 사용된다. 즉, 싱글톤 마냥 사용되는 것이다.
// 하지만 매번 새로운 프로토 타입을 주입 받고 싶은게 목적이다. 어떻게 해야 할까?
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
이러한 방식을 Dependency Lookup(DL)
이라고 한다.
의존 관계를 주입 받는 것이 아닌 컨테이너에서 조회를 한다는 용어이다.
Provider 사용하기
Dependency Lookup(DL)
을 위에서 사용 했다.
그렇다면 컨테이너를 주입 받지 않고 Dependency Lookup(DL)
을 구현할 수 없을까? 있다. Provider
이다.
스프링에는 DL을 제공하는 ObjectFacotry, ObjectProvider
가 있다.
이 두가지 인터페이스는 딱!!! DL의 기능만 제공하는 인터페이스이다.
아래 코드와 같이 컨테이너를 주입 받기 않고 조회를 할 수 있게 된다.
static class ClientBean {
private final ObjectProvider<PrototypeBean> prototypeBeanProvider;
ClientBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
정리
- 컨테이너에서 특정 빈을 찾는 기능(DL)만 제공하므로 컨테이너에 의존하지 않는다.
- 결국 단순해지므로 테스트가 쉬워진다.
- ObjectFatory : 오직 getObject()만 제공한다. 스프링에 의존, 라이브러리 x
- ObjectProvider : ObjectFactory를 상속 받은 인터페이스로 편리 기능을 제공한다. 스프링에 의존, 라이브러리 x
Provider는 이와 같은 상황에서 프로토타입 빈을 새로 생성하기 위해 만들어진 것이 아니고,핵심은 컨테이너를 거치지 않고 빈을 조회하기 위한 것이고, .getObject()할 때 주입 받도록 지연을 위한 것이다.
JSR-330(javax) Provider
스프링의 ObjectFactory와 동일하다고 보면 된다.
메서드명만 get()으로 변경하면 되기 때문에 사용법은 동일하다.
JSR-330 Provider, ObjectFactory, ObjectProvider중 뭘 써야하지?
- 일단 ObjectFactory와 ObjectProvider 중에서는 특별한 상황이 아닌 이상 ObjectProvider를 쓰는게 좋다고 생각한다.
아무래도 편의 기능들이 있기 때문이다. - JSR-330 Provider와 스프링 Provider의 비교는 자바 표준과 스프링으로 비교를 해보면 좋을 것 같다.
자바 표준보다 스프링 기능이 더 좋다고(예로 필요한 기능이 더 있다던지) 생각이 들면 스프링을 선택하고,
스프링에서 자바 표준을 권장하면 (예를 들어 @PostConstruct) 자바 표준을 사용하는 것이 좋을 것 같다.
만약 스프링 컨테이너에서 → 다른 컨테이너로 바뀔 일이 있다면 자바 표준을 따르는게 좋겠지만, 그럴 일이 있을가 싶다.
사실상 자바 웹 프레임워크의 표준이 스프링이기 때문에 대부분의 상황은 스프링의 기능을 사용하게 될 것이다.