지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료 될때까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. scope란 단어의 뜻 그대로 스프링 빈이 존재할 수 있는 범위를 의미한다. 즉, 생존할 수 있는 기간을 뜻한다.
🔻 스프링은 다음과 같은 다양한 스코프를 지원한다.
🔻 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class PrototypeBean {}
🔻 수동 등록
@Scope("prototype")
@Bean
PrototypeBean prototypeBean(){
return new PrototypeBean();
}
싱글톤 스코프 타입의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.반면에 프로토타입 스코프 타입의 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성하고 해당 인스턴스를 반환한다.
🔻 싱글톤 스코프 의 스프링 빈 요청
여기서 프로토타입은 싱글톤 타입의 스피링 빈과는 다르게 빈 생성, 의존관계 주입, 초기화까지만 진행한다. 그렇기에 그 이후 스프링 빈을 클라이언트에 반환한 이후로는 관리하지 않기에 소멸 메서드같은것은 모두 클라이언트에서 자체적으로 관리해야 한다.
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
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.Assertions.assertThat;
public class SingletonTest {
@Test
public void singletonBeanFind() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
System.out.println("singletonBean1 = " + singletonBean1);
System.out.println("singletonBean2 = " + singletonBean2);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton")
static class SingletonBean{
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
싱글톤 스코프의 스프링 빈은 여러번 호출해도 모두 같은 인스턴스 참조 주소값을 가진다. 스프링 컨테이너 종료시 소멸 메서드도 자동으로 실행된다.
package hello.core.scope;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
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.Assertions.assertThat;
public class PrototypeTest {
@Test
public void prototypeBeanFind() {
ConfigurableApplicationContext 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");
}
}
}
🔻 싱글톤 스프링 빈 내부에 의존관계로 주입되는 스프링 빈이 프로토타입인 경우
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.Assertions.assertThat;
public class SingletonWithPrototypeTest1 {
@Test
void singletonClientUserPrototype() {
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);
}
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");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
PrototypeBean은 프로토타입 스코프지만 clientBean은 싱글톤 스코프이기 때문에, 싱글톤 빈에서 프로토타입 빈을 사용한다. 싱글톤 빈의 스코프는 스프링 컨테이너와 같은데, 프로토타입 스코프의 스프링 빈이 새로 생성되기는 했지만 싱글톤 빈과 함께 사용되기 때문에 계속 유지된다.
그래서 빈을 2회 요청하지만 동일한 프로토타입 빈을 사용하게되어 count는 1이 아닌 2가된다. 프로토타입 빈만 클라이언트가 직접 사용하는경우라면 상관없지만 싱글톤 빈과 함께 사용하면서 프로토타입 빈이 자기의 스코프를 지키고 매번 새롭게 생성하기 위해서는 어떻게 해야할까?
static class ClientBean{
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
ObjectFactory: 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공해준다. 아주 단순하게 getObject 하나만 제공하는 FunctionalInterface이고, 별도의 라이브러리도 필요 없다. 그리고 스프링에 의존한다.
ObjectProvider : ObjectFactory에 편의기능들(Optional, Stream...)추가해서 만들어진 객체
별도의 라이브러리는 필요 없고 스프링에 의존한다.
적용 코드
static class ClientBean{
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
위에서 실행한 ac.getBean(PrototypeBean.class) 와 동일하게 매번 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
스프링에 종속적인것은 동일하지만, 기능이 단순해서 단위테스트 및 Mock 을 이용한 테스트 더블을 준비하기 쉽다.
이런 스프링의 의존성이 마음에 들지 않으면 javax.inject.Provider 패키지의 JSR-330 자바 표준을 사용하는 방법이 있다. 이 방법을 사용하기 위해서는 javax.inject:javax.inject:1 라이브러리를 추가
🔻 build.gradle에 라이브러리 추가
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'javax.inject:javax.inject:1'
...
}
🔻 테스트 코드 변경
import javax.inject.Provider;
...
static class ClientBean{
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
웹 환경에서만 동작하는 스코프로 스프링이 해당 스코프의 종료시점까지 관리하며, 종료 메서드도 호출된다.
1. build.gradle에 web 환경 추가
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
만약 실행했을때 기본 포트인 8080 포트가 사용중이라면 main/resources/application.properties에서 다음 내용을 추가해 포트를 변경하자.
server.port=9000
2. 코드 작성
3. 실행
: 이제 테스트를 위해 CoreApplication을 실행시켜보면, 예상과 다르게 에러가 발생할 것이다.
스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않고 실제 고객의 요청이 와야 생성할수 있기 때문이다.
다시말해, 해당 프로젝트가 구동될때 스프링 빈들이 컴포넌트 스캔이 되며 등록 및 의존관계 주입이 되는데, 여기서 웹스코프인 MyLogger 빈의 경우 HTTP request 요청이 올때 생성되는 빈이기 때문에 스프링 구동단계에서는 아직 생성을 할 수 없다. 그렇기에 해당 에러가 발생하는 것이다.
정리하면, 스프링 구동시 MyLogger 스프링 빈을 등록을 요구하는데 해당 빈은 자신이 생성되야 할 스코프 범위에 해당되지 않았기 때문에 에러가 발생한다.
그럼 해당 스프링 빈은 스프링 구동시점이아닌 사용자의 HTTP request 요청 시점에 생성될 수 있다는 말인데, 이를 해결하기위한 방법들에 대해 알아보자.