Study/Spring
[Spring] 싱글톤 스코프에서 프로토타입 스코프 사용 시 문제점과 해결 방법
lsh2613
2023. 8. 1. 23:13
먼저 싱글톤과 프로토타입의 차이점에 대해 알아보자
싱글톤 VS 프로토 타입
싱글톤
- 스프링 컨테이너 생성 시 빈 객체가 같이 생성되며 컨테이너 종료 시 같이 소멸한다.
- 객체 인스턴스가 한 번만 생성되고 각 클라이언트에서 같은 객체를 공유하여 사용한다
프로토타입
- 스프링 컨테이너가 종료되고 같이 소멸하지 않고, 클라이언트에서 더 이상 사용되지 않으면 가비지 컬렉터에 의해 수거된다.
- 클라이언트에서 호출 될 때마다 새로운 객체를 반환한다.
프로토 타입 설정과 테스트
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class PrototypeTest {
@Test
public void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close(); //종료
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
} }
코드 풀이
@Scope를 통해 프로토 타입을 지정할 수 있다.
해당 프로토타입의 빈을 getBean을 통해 꺼냈을 때 서로 다른 객체임을 확인할 수 있다.
문제가 발생하는 코드
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class SingletonWithPrototypeTest1 {
@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(2);
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
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");
}
}
}
코드 풀이
싱글톤인 ClientBean에서 PrototypeBean을 의존관계 주입을 받아 사용하고 있다.
테스트 코드에서는 분명 ClientBean을 두 번 호출하여 각각 로직을 수행하였을 때 count값을 증가하였지만 count값이 공유되고 있음을 확인할 수 있다.
그 이유는 싱글톤인 ClientBean에서 생성 시점에 PrototypeBean에 대한 의존관계가 이미 이루어졌기 때문에 그렇다.
prototype은 호출 시 새로운 객체를 사용하기 위해 사용하는 건데 위 문제를 해결하는 방법은 뭐가 있을까?
해결 방법
1. 스프링 프레임워크에서 지원하느 ObjectProvider
2. 자바 표준에서 지원하는 javax.inject.Provider
2.1 스프링 3.0 이상에서는 jakarta.inject.Provider
1. ObjectProvider
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
2. Provider
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
DL (Dependency Lookup)
이처럼 외부에서 의존관계를 주입하는 것이 아니라 직접 필요한 의존관계를 찾는 것을 말한다.