빈 스코프 = 빈이 존재할 수 있는 범위
기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다. 반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다. 그리고 반환하고 나서는 스프링 컨테이너에서는 가지고 있지 않는다.
프로토타입 빈을 요청하는 시점에 생성하고, 의존관계 주입해서 반환 후 새로운 요청이 오면 또 새로 만들어서 반환한다.
-> 클라이언트가 책임지고 종료메서드를 호출해 줘야한다.
-> 그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.
package com.example.springframework.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
void prototypeBeanFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
System.out.println("bean1 = " + bean1);
System.out.println("bean2 = " + bean2);
assertThat(bean1).isNotSameAs(bean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
1) close가 작동되지 않음!
-> 만약 종료를 호출하고 싶다면
bean1.destroy();
bean2.destroy();
이런 식으로 직접 호출해줘야 한다.
2) 서로 다른 bean이 생성된 것을 알 수 있음 -> init도 2번 호출됨
그럼 프로토타입으로 구현된 빈이 있고 그 빈은 count=0이라는 속성을 가지고 있다고 하자. 그리고 addCount()를 통해서 값이 1만큼 올라갈 수 있을때,
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(prototypeBean.class);
prototypeBean bean1 = ac.getBean(prototypeBean.class);
prototypeBean bean2 = ac.getBean(prototypeBean.class);
bean1.addCount();
System.out.println(bean1.getCount());
assertThat(bean1.getCount()).isEqualTo(1);
bean2.addCount();
System.out.println(bean2.getCount());
assertThat(bean2.getCount()).isEqualTo(1);
}
@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");
}
}
}
위 테스트는 통과가 된다. 즉 각각 1씩 오른 것이다.(싱글톤이 아니니깐 당연한지도!)
그렇다면 만약 clientBean(싱글톤)이 프로토타입빈에 의존하고 있는 경우에도 다른 사용자의 요청에 각각 다르게 프로토타입빈이 응답할 수 있을까?
정답은 아니다! 왜냐하면 clientBean이 생성될때 의존관계 주입을 통해서 생성된 프로토타입빈을 주입받으면, 그 주입받은 것만을 가지고 있기 때문이다.
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;
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");
}
}
}
만약 싱글톤으로 구현하면서 매번 고객이 다를때마다 다른 응답을 하고 싶다면,
ApplicationContext를 직접 주입받아서 logic()메서드를 실행할때마다 application.getBean(Prototype.class)를 가져와서 실행해주면 된다.
@Scope("singleton")
static class ClientBean2 {
@Autowired
ApplicationContext applicationContext;
public int logic() {
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
하지만 이 방법은 코드가 지저분해지므로 지양된다.
의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.
그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 된다.
@Scope("singleton")
static class ClientBean3 {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
ObjectProvider를 사용하면 .getObject()시점에 스프링컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
이렇게 하면 좋은 점이 ApplicationContext의 전체 기능을 사용하는 것이 아니라, 필요한 기능만 짤라서 사용하기 때문에 스프링 컨테이너에 대해 의존적이지 않게 된다.
implementation 'javax.inject:javax.inject:1'
import javax.inject.Provider;
//javax.inject.provider
@Scope("singleton")
static class ClientBean4 {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
get() 메서드 하나로 기능이 매우 단순하다.
별도의 라이브러리가 필요하다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
http요청시 생성되고 요청이 끝나면 소멸됨. 요청당 생성된다