싱글톤 빈과 함께 사용시 Provider로 문제 해결
싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을 까?
방법1. .스프링 컨테이너에 요청
가장 간단한 방법은 싱글톤 빈이 프로토 타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
import static org.assertj.core.api.Assertions.assertThat;
public class PrototypeProviderTest {
@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 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
//스프링 컨테이너에 요청.. 요청할 때마다 새로운 스프링 컨테이너 생성
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); // 의존관계 주입(DI)
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
@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");
}
}
}
- 실행해보면 ac.getBean() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- 의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.
- 그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
- 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가 가 있으면 된다.
그게 바로 스프링의 ObjectProvider 이다.
방법2. ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.
참고로 과거에는 ObjectFactory 가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어졌다.
@Scope("singleton")
static class ClientBean {
// 이전 코드 : 프로토 타입을 사용할 때 마다 스프링 컨테이너에 새로 요청
// @Autowired
// private ApplicationContext ac;
//
// public int logic() {
// PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
// prototypeBean.addCount();
// int count = prototypeBean.getCount();
// return count;
// }
// ObjectProvider 사용
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- 실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것 을 확인할 수 있다.
- ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 한다. (DL)
- (DL) 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
- ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.
- 스프링 컨테이너 조회를 하는데 직접 찔러서 조회하는 것보다 얘를 통해서 조회해주는 대리자 정도?
ObjectProvider 특징
- ObjectFactory가 부모
- ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
방법3. JSR-330 Provider
javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.
스프링 부트 3.0은 jakarta.inject.Provider 사용한다.
라이브러리 추가
이 방법을 사용하려면 다음 라이브러리를 gradle에 추가해야 한다.
스프링부트 3.0 미만
javax.inject:javax.inject:1
스프링부트 3.0 이상
jakarta.inject:jakarta.inject-api:2.0.1
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'jakarta.inject:jakarta.inject-api:2.0.1' // jakarta.inject.Provider 사용을 위해 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter'
//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
//lombok 라이브러리 추가 끝
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
주의❗ import jakarta.inject.Provider;을 가져와야 한다!! inject!!!
// 이전 코드 2. ObjectProvider
// @Autowired
// private ObjectProvider<PrototypeBean> prototypeBeanProvider;
//
// public int logic(){
// PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
// prototypeBean.addCount();
// int count = prototypeBean.getCount();
// return count;
// }
// 3. JSR-330 Provider
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider; // ObjectProvider > Provider로 변경
// ❗ import jakarta.inject.Provider;을 가져와야 한다!! inject!!!
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get(); // getObject() > get()로 변경
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
- 실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
- Provider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.
Provider는 가진게 get()밖에 없다.
package jakarta.inject;
/**
* Provides instances of {@code T}. Typically implemented by an injector. For
* any type {@code T} that can be injected, you can also inject
* {@code Provider<T>}. Compared to injecting {@code T} directly, injecting
* {@code Provider<T>} enables:
*
* <ul>
* <li>retrieving multiple instances.</li>
* <li>lazy or optional retrieval of an instance.</li>
* <li>breaking circular dependencies.</li>
* <li>abstracting scope so you can look up an instance in a smaller scope
* from an instance in a containing scope.</li>
* </ul>
*
* <p>For example:
*
* <pre>
* class Car {
* @Inject Car(Provider<Seat> seatProvider) {
* Seat driver = seatProvider.get();
* Seat passenger = seatProvider.get();
* ...
* }
* }</pre>
*/
public interface Provider<T> {
/**
* Provides a fully-constructed and injected instance of {@code T}.
* @return instance of {@code T}.
*
* @throws RuntimeException if the injector encounters an error while
* providing an instance. For example, if an injectable member on
* {@code T} throws an exception, the injector may wrap the exception
* and throw it to the caller of {@code get()}. Callers should not try
* to handle such exceptions as the behavior may vary across injector
* implementations and even different configurations of the same injector.
*/
T get();
}
특징
- get() 메서드 하나로 기능이 매우 단순하다.
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
정리
그러면 프로토타입 빈을 언제 사용할까? 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사 용하면 된다.
그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때 문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
❓ 언제 ObjectProvider와 Provider(JSR-330 Provider)를 구분해서 사용하는가?
만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.
참고: 스프링이 제공하는 메서드에 @Lookup 애노테이션을 사용하는 방법도 있지만, 이전 방법들로 충분하고, 고려해야할 내용도 많아서 생략하겠다.
참고: 실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider 를 사용할 것인지 고민이 될 것이다. ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별 도의 의존관계 추가가 필요 없기 때문에 편리하다. 만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.
스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많 이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없 다면, 스프링이 제공하는 기능을 사용하면 된다.
'공부 > Spring' 카테고리의 다른 글
@JsonInclude(JsonInclude.Include.NON_NULL) (0) | 2024.01.19 |
---|---|
[Spring] 빈 스코프 - 웹 스코프 (2) | 2024.01.09 |
[Spring] 빈 스코프 - 프로토타입 스코프와 싱글톤 빈과 함께 사용시 문제점 (0) | 2024.01.02 |
[Spring] 빈 스코프 - 빈 스코프(싱글톤 스코프, 프로토타입 스코프) (1) | 2023.12.31 |
[Spring] 빈 생명주기 콜백 - 빈 생명주기 콜백 지원 3가지(@PostConstruct, @PreDestroy) (1) | 2023.12.31 |