※본 글은 김영한님의 '자바 스프링 완전정복 시리즈' 강의를 바탕으로 작성한 글입니다.
스프링에서 빈을 기본적으로 싱글톤으로 관리하고, 대부분의 경우 싱글톤이 필요하지만, 그렇다고 싱글톤만 사용되는 것은 아닙니다.
그 외에도 다양한 스코프가 존재하고, 이번에는 프로토타입 스코프에 대해서 정리해보겠습니다.
프로토타입 스코프는 스프링 컨테이너가 ①빈의 생성까지만 관여하는 스코프로, ②요청 시 매번 새로운 인스턴스를 생성하게 됩니다.
프로토타입 스코프로 관리되는 클래스인 ProtoType 클래스를 2번 조회하는 테스트를 진행해보겠습니다.
package hello.core.prototype;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
public class prototypeTest {
@Test
void instanceTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
ProtoType bean1 = ac.getBean(ProtoType.class);
bean1.count();
ProtoType bean2 = ac.getBean(ProtoType.class);
bean2.count();
Assertions.assertThat(bean2).isNotSameAs(bean1);
}
@Configuration
static class Config{
@Bean
@Scope("prototype")
public ProtoType protoType(){
return new ProtoType();
}
}
static class ProtoType{
static int count;
public ProtoType(){
count = 0;
}
public int count(){
System.out.println("count : " + ++ count);
return count;
}
}
}
ProtoType 클래스를 2번 조회했을 때, 프로토타입 스코프이므로 서로 다른 객체가 반환되어 테스트를 통과하게 됩니다.
프로토 타입 스코프 빈은 스프링 컨테이너가 생성까지만 관여합니다. 즉, 생성 - 의존관계 주입 - 초기화 메서드 호출 까지만 관리하므로, 소멸 메서드를 호출해야 된다면 클라이언트에서 직접 호출해야 합니다.
먼저 다음 코드를 보겠습니다.
앞서 작성한 ProtoType 클래스에 의존하는 ClientBean 클래스를 추가로 작성했습니다. 이때, ClientBean은 싱글톤 스코프로 관리되는 클래스입니다.
package hello.core.prototype;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
public class prototypeTest {
@Test
void instanceTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
ClientBean client1 = ac.getBean(ClientBean.class);
client1.count();
ClientBean client2 = ac.getBean(ClientBean.class);
client2.count();
}
@Configuration
static class Config{
@Bean
@Scope("singleton")
public ClientBean clientBean(){
return new ClientBean(protoType());
}
@Bean
@Scope("prototype")
public ProtoType protoType(){
return new ProtoType();
}
}
static class ClientBean{
private ProtoType protoType;
public ClientBean(ProtoType protoType){
this.protoType = protoType;
}
public int count(){
return protoType.count();
}
public ProtoType getProtoType(){
return protoType;
}
}
static class ProtoType{
static int count;
public ProtoType(){
count = 0;
}
public int count(){
System.out.println("count : " + ++ count);
return count;
}
}
}
ProtoType에 의존하는 ClientBean은 싱글톤 빈으로 관리됩니다.
그렇기 때문에 ProtoType() 자체는 프로토타입이더라도, ClientBean에는 주입이 한번만 이뤄지기 때문에 계속 같은 ProtoType의 객체를 사용하게 됩니다.
따라서 client1.count의 결과는 1이고, client2.count의 결과는 2가 됩니다.
프로토타입으로 설정했는데, 마치 싱글톤처럼 동작하고 있습니다.
싱글톤 내에서 의존하는 클래스의 인스턴스를 사용할 때마다 새롭게 생성하고 싶다면 어떻게 해야할까요?
이번에도 코드를 먼저 보겠습니다.
package hello.core.prototype;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Component;
public class prototypeTest {
@Test
void instanceTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
ClientBean client1 = ac.getBean(ClientBean.class);
client1.count();
ClientBean client2 = ac.getBean(ClientBean.class);
client2.count();
}
@Configuration
@ComponentScan
static class Config{
}
@Component
@Scope("singleton")
static class ClientBean{
// private ProtoType protoType;
private ObjectProvider<ProtoType> protoTypeProvider;
@Autowired
public ClientBean(ObjectProvider<ProtoType> protoTypeProvider){
this.protoTypeProvider = protoTypeProvider;
}
public int count(){
ProtoType protoType = protoTypeProvider.getObject();
return protoType.count();
}
public ProtoType getProtoType(){
ProtoType protoType = protoTypeProvider.getObject();
return protoType;
}
}
@Component
@Scope("prototype")
static class ProtoType{
static int count;
public ProtoType(){
count = 0;
}
public int count(){
System.out.println("count : " + ++ count);
return count;
}
}
}
ProtoType에 직접 의존하지 않고, ObjectProvider<ProtoType> 에 의존하고 있습니다.
클래스의 이름대로, ProtoType을 매번 새로 제공해주는 클래스에 의존하는 방법입니다.
이렇게 하면, 프로토타입으로 설계한 의도대로 매 사용마다 인스턴스를 꺼내서 사용할 수 있습니다.
ObjectProvider와 동일한 원리입니다.
자바 표준인 javax.inject.Provider를 사용하는 방법입니다.
Provider<ProtoTypeBean> 에 의존하고, 필요할 때마다 .get() 을 통해 꺼내서 사용하면 됩니다.
※ 위 두 방식처럼 컨테이너에서 필요한 객체를 찾아오는 것을 'DL(Dependency Lookup) 방식'이라고 합니다