[Spring]프로토 타입 빈 사용시 생기는 문제점 해결하기(ObjectProvider와 JSR-330 Provider)
1. 문제 상황
흔히 싱글톤 빈 내부에 프로토타입 빈을 두고 사용할때
이 프로토타입 빈이 싱글톤마냥 관리되면서 요청할 때마다 새로 생성되는 것이 아니라
의존관계 주입 시점에 처음 생성되고 계속 재활용 되는 문제에 직면하는 경우가 생기고는 한다.
간단한 예제 상황을 가정하여 count라는 정수를 가지고 있으면서
클라이언트가 count에 1을 더해주는 addCount, count값을 반환해주는 getCount 프로토 타입 빈이 있다고 해보자.
이 프로토타입 빈을 스프링 컨테이너에 여러번 요청해서 받은 각각의 빈들에게
addCount해주고 값들을 받아보면 어떻게 될까?
아래는 그 예시 코드이다.
public class SingletonWithPrototypeTest {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac=new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
bean1.addCount();
Assertions.assertThat(bean1.getCount()).isEqualTo(1);
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
bean2.addCount();
Assertions.assertThat(bean2.getCount()).isEqualTo(1);
}
@Scope("prototype")
static class PrototypeBean {
private int count=0;
@PostConstruct
void init() {
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
void preDestroy() {
System.out.println("PrototypeBean.preDestroy"+this);
}
public void addCount() {
count++;
}
public int getCount() {
return count;
}
}
}
당연히 돌려보면 getCount의 결과는 모두 1이 뜨면서 테스트는 성공할 것이다.
그러면 이러한 prototypeBean이 싱글톤 빈 내부에 존재하면 어떻게 될까?
또, 각각의 싱글톤 빈을 사용하는 클라이언트들이 프로토타입 빈의 addCount를 사용하게 되면 어떻게 될까?
아래는 그 예시 코드이다.
우선 싱글톤 빈을 두개 컨테이너에 요청해서 받아온 뒤
각각 싱글톤 빈 내부의 logic함수를 요청해서 결과를 비교해보는 코드이다.
(즉 싱글톤 빈 내부에 있는 프로토타입 빈이 컨테이너에 요청해서 받을 때 처음 프로토타입이 그대로 사용되는지, 아니면 새로 생성이 되는지 비교해 보는것!)
public class SingletonWithPrototypeTest {
@Test
void singletonClientUsePrototypeBean() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class,PrototypeBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
int count = singletonBean1.logic();
Assertions.assertThat(count).isEqualTo(1);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
int count2 = singletonBean1.logic();
Assertions.assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
static class SingletonBean {
private final PrototypeBean prototypeBean;
@Autowired
public SingletonBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
System.out.println("사용할 prototypeBean 정보!: "+prototypeBean);
prototypeBean.addCount();
return prototypeBean.getCount();
}
@PostConstruct
void init() {
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
void preDestroy() {
System.out.println("PrototypeBean.preDestroy"+this);
}
}
@Scope("prototype")
static class PrototypeBean {
private int count=0;
@PostConstruct
void init() {
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
void preDestroy() {
System.out.println("PrototypeBean.preDestroy"+this);
}
public void addCount() {
count++;
}
public int getCount() {
return count;
}
}
}
위 테스트 코드를 돌려보면 테스트가 성공하는 것을 볼 수 있고 아래와 같이 로그가 찍힌다.
위 코드를 보면 싱글톤 빈 안에 있다는 이유로
싱글톤 빈 내부에 있는 프로토 타입 빈까지 싱글톤처럼 관리되어버리는 것을 볼 수 있다.
대부분 프로토타입 빈을 지정하는 것은 요청할 때마다 새로운 빈을 생성해주길 바라기 때문에
이러한 방식으로 동작하는 것은 원하지 않을 것이다.
그렇다면 싱글톤 빈 내부의 프로토타입 빈을 요청할 때마다 새롭게 생성되게 하려면 어떻게 해야 할까?
2. 해결법
2-1. Spring에서 제공해주는 ObjectProvider 사용하기
ObjectProvider는 스프링에서 제공해주는 빈을 컨테이너에 요청하면 찾아주는 기능(이를 Dependency Lookup라고 부른다.)을 제공해준다.
사용하는 방법은
ObjectProvider를 스프링에게 주입받은 다음 getObject 메서드를 사용 시 스프링 컨테이너에게 해당 빈을 요청하여 받아올 수 있다.
아래는 위의 예시 코드를 ObjectProvider를 사용하도록 바꾼 코드이다.
@Scope("singleton")
static class SingletonBean {
@Autowired
private ObjectProvider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.getObject();
System.out.println("사용할 prototypeBean 정보!: "+prototypeBean);
prototypeBean.addCount();
return prototypeBean.getCount();
}
@PostConstruct
void init() {
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
void preDestroy() {
System.out.println("PrototypeBean.preDestroy"+this);
}
}
위 코드를 돌려보면 아래와 같은 로그가 뜬다.
사용할 prototypeBean정보 로그를 보면 사용할 때마다 provider에게 새로운 빈을 받아 사용하는 것을 볼 수 있다.
- 참고
ObjectProvider와 비슷한 기능을 하는 ObjectFactory라는 녀석도 있는데 두 클래스의 차이점은 다음과 같다.
- ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존한다.
- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 마찬가지로 스프링에 의존한다.
2-2. Java 표준 기술인 JSR-330 Provider 사용하기
이 방법은 스프링에 의존하지 않고 자바에서 제공해주는 별도 라이브러리를 받아서 DL기능을 사용하는 방법이다.
먼저 아래의 라이브러리를 gradle에 추가해준다.
dependencies {
...
implementation 'javax.inject:javax.inject:1'
...
}
라이브러리를 무사히 가져왔다면 코드를 아래와 같이 수정한다.
@Scope("singleton")
static class SingletonBean {
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
System.out.println("사용할 prototypeBean 정보!: "+prototypeBean);
prototypeBean.addCount();
return prototypeBean.getCount();
}
@PostConstruct
void init() {
System.out.println("PrototypeBean.init"+this);
}
@PreDestroy
void preDestroy() {
System.out.println("PrototypeBean.preDestroy"+this);
}
}
Provider를 쓸 때 주의할 점이 import하는 클래스가 여러개 나오는데 그 중 아래의 inject에서 제공해주는 provider를 사용해야 한다.
import javax.inject.Provider;
이렇게 하면 마찬가지로 prototypeBean을 요청할 때마다 새로운 빈을 받아서 사용하는 것을 볼 수 있다.
참고로 이 자바 표준의 provider라이브러리를 들어가보면 아래와 같이 코드가 나오는 것을 볼 수 있는데
public interface Provider<T> {
/**
* Provides a fully-constructed and injected 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메서드 하나만 제공해주고 있는 아주 단순한 기능만 제공해주고 있는 것을 알 수 있다.
또, 이 방법은 스프링에 의존하지 않으므로 스프링 컨테이너가 아닌 타 컨테이너에서도 사용 가능하다는 장점이 있다.
단점으로는 gradle에 별도의 라이브러리를 추가해줘야 한다는 정도일 것 같다.
Reference
이 포스팅은 아래의 강좌를 참고하여 만들어졌습니다.
- 스프링 핵심 원리-기본편
'Backend > Spring' 카테고리의 다른 글
[Spring]AOP의 개념 및 적용 예제(공통관심사 처리하기) (1) | 2020.11.23 |
---|---|
[Spring]Request Scope를 사용해서 깔끔하게 로그남기기 (0) | 2020.11.19 |
[Spring]싱글톤 빈 VS 프로토 타입 빈 차이점 (0) | 2020.11.11 |
[Spring]스프링에서 공통 Response처리 하기(@ControllerAdvice 이용) (0) | 2020.11.04 |
ipTIME의 DDNS를 이용하여 운영 서버 구축하기(포트포워딩) (1) | 2020.10.30 |