스프링빈 스코프 : 빈이 존재할 수 있는 범위~~
스프링빈 Scope 종류
∨ 싱글톤
: 기본 스코프 & 젤 긴 스코프... 지금까지 실습했던 것이 다 싱글톤 스코프..
컨테이너 시작 후 빈생성 ~ 스프링 컨테이너 종료 전 빈소멸
∨ 프로토타입
: 컨테이너가 빈생성&DI&(초기화콜백)까지 관여하고 그 이후는 관리 안한다. 매우짧은 스코프!
∨ 웹 관련 스코프
- request : 사용자로부터 웹요청 들어오고 나갈때까지 유지되는 스코프
- session : 웹세션 생성~종료 스코프
- application : 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
+ 스코프 지정 방법
그냥 @Scope("~~") 붙여주면 된다!
예)
- 컴포넌트 스캔 자동 등록 경우
@Scope("prototype")
@Component
- 수동 빈 등록인 경우
@Scope("prototype")
@Bean
Scope가 @Component나 @Bean의 위로가든 아래로가든 상관 없다.
그냥 @Scope라는 annotation만 잘 붙여주면 된다.
∨ 프로토타입 스코프
싱글톤 스코프는 계속 겪어봤으니까 프로토타입 스코프를 공부하자.
그냥.. 컨테이너가 관리 안하는 빈의 스코프다..
그때그때 빈 생성해서 던져 주는 거다! 생성/DI/초기화콜백만 하고 던진다.
=> 매번 다른 인스턴스를 던져준다.
스프링 컨테이너가 관리하는 빈이 아니기 때문에 종료콜백 호출 책임은 없다.
=> 즉 @PreDestroy가 호출되지 않는다.
프로토타입 빈을 받은 클라이언트에 빈 관리 책임이 있다.
예제
싱글톤빈은 매번 같은 객체를 던져준다.
컨테이너에서 관리하는 그 싱글톤 객체를 던지는 거니까 매번 동일한 객체다.
ㅇ 싱글톤 빈
@Scope("singleton") <- 디폴트라 안써도 됨
static class SingletonBean{
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
cf) AnnotationConfigApplicationContext(SingletonBean.class)
: 파라미터로 클래스 하나가 들어가있는데, 파라미터명을 까보면 componentClasses라고 적혀있다. 여기 파라미터로 직접 들어가는 클래스는 자동으로 컴포넌트가 되어서 스프링빈으로 등록된다.
* ctrl + P : 파라미터 정보 볼 수 있음
* ctrl + D : duplicate
ㅇ 테스트
Assertions.assertThat(singletonBean1).isSameAs(singletonBean2) 검증에 성공했다
@Test
void singletonBeanFine(){
AnnotationConfigApplicationContext 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);
Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);
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");
}
}
∨ 테스트
assertThat(prototypeBean1).isNotSameAs(prototypeBean2) 검증 완료했다.
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find prototypeBean1");
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find prototypeBean2");
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close();
}
∨ 테스트를 육안으로 확인해보려면 아래와 같다.
: ac.getBean(PrototypeBean.class)할 때마다 초기화콜백 init이 호출되고 있다.
: prototypeBean1, prototypeBean2는 다른 객체임을 확인가능하다.
: ac.close()가 테스트코드에 존재하는데도 종료콜백이 호출되지 않았다. 프로토타입빈이기 때문이다.
프로토타입 빈의 종료콜백 책임은 컨테이너가 가지고 있지 않다고 했다.
클라이언트쪽에서 프로토타입 빈을 관리하므로, 종료콜백이 필요하다면 직접 호출 하면 된다.
prototypeBean1.destroy();
prototypeBean2.destroy();
ac.close();
주의 >_<
싱글톤 빈에서 프로토타입 빈 의존관계 주입을 받아서 사용하고 싶을 경우 어떻게 해야 할까?
즉, 싱글톤 빈의 필드로 프로토타입 빈을 두고, 프로토타입 빈 메서드를 사용할 때마다 프로토타입 스코프 원리&의도대로 매번 다른 빈을 생성하여 사용하고 싶을때 어떻게 해야 옳을까?
참고)
실무에서는 거의 싱글톤 빈으로 대부분 해결이 되기 때문에 프로토타입 빈 직접 사용은 드물다.
잘못된 예) 그냥 필드에 프로토타입빈 주입받아 쓰겠다.
즉: 싱글톤 빈인 class clientBean에서 프로토타입 빈인 private final PrototypeBean prototypeBean 을 가지고 있는 상태를 말한다. 다시말해 prototypeBean은 컨테이너가 관리하지 않고, 그냥 clientBean 내부 필드에 참조값을 보관하는 형태다.
잘못된 예시인 이유: 싱글톤 빈 생성할 때, 필드인 프로토타입 빈을 컨테이너가 생성&DI&초기화 하고 그냥 던져주면 거기서 끝이다. 만약 싱글톤빈의 로직을 통해 프로토타입 빈의 메서드를 호출한다고 해도 계속 동일한 (프로토타입)빈에 적용이 된다. 싱글톤빈 생성시점에 주입된 (프로토타입)빈은 이젠 그냥 단독 필드로서의 빈으로 존재할 뿐 뭐 클라이언트가 요청할때마다 또다른 프로토타입 빈이 또 생성되거나 하는게 아니다.
당연해보인다!
무식한 방법) 매번 컨테이너에 요청하기
필드로 ApplicationContext applicationContext 하나 두고 @Autowired로 스프링 컨테이너 잘 주입받고나서,
호출할 logic() 메서드에서 applicationContext.getBean(PrototypeBean.class)으로 매번 다른 프로토타입 빈 생성해내서 쓰면 의도한대로 프로토타입빈으로서 잘 기능할 것이다.
그러나 ClientBean(싱글톤빈)가 컨테이너를 직접 가져와 쓰는게 지저분하다.
* 싱글톤 빈 ↔ 프로토타입 빈 의존관계가 필요해서 컨테이너에서 직접 생성해 주입하고 있다.
이때 의존관계를 외부에서 주입하려는 목적이 아니다! (DI가 아니다) 그냥 싱글톤빈에서 프로토타입빈이 필요하니까 직접 의존관계를 찾아쓰는 것이다.
이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL), 의존관계 조회(탐색)이라고 한다.
* 그런데 이 무식한 방법에서는 DL을 위해 스프링 컨테이너 전체를 @Autowired로 가져다가 쓰고 있다. 이렇게 컨테이너 전체를 주입받게 되면 컨테이너에 종속적이게 되고 단위테스트가 어려워진다.
지저분하게 컨테이너 자체를 가져와서 필요한 프로토타입 빈을 DL하지는 말자.
↓ 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 기능만 딱 제공하는 무언가만 있으면 된다.
↓
↓
조은 방법) Provider로 해결 (스프링 컨테이너에 요청하기)
스프링 컨테이너를 통으로 가져다 쓰는게 아니라 딱 필요한 기능만 요청해서 쓸 수 있다.
Provider 개념인데, 컨테이너의 대리자 느낌으로 딱 필요한 기능만 쓸 수 있어서 좋다.
자세한건 pdf에서 복습하도록..
Provider의 getObject()를 통해 PrototypeBean을 가져올 수 있다.
테스트코드 한번 만들어서 확인해보면, 매번 다른 PrototypeBean을 생성해 쓰는 걸 확인 가능하다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
// ↑↑ 그냥 예시로는 간단히 필드주입을 하고있는데 생성자주입을 권장한다
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
위 예시와 같이 스프링에 의존하는 ObjectProvider(ObjectFactory를 상속받음)를 쓸 수도 있지만
스프링에 의존하지 않는 Provider도 존재한다!
: javax.inject.Provider을 이용! JSR-330 자바 표준이다. 가장 심플하다.
: 단, 라이브러리를 gradle에 추가해 써야한다. implementation 'javax.inject:javax.inject:1'
기능이 매우 단순! get()으로 컨테이너에 빈 찾아달라고 하면 된다.
딱 DL 기능만 제공하기 좋겠다.
단위테스트, mock 코드 만들기가 훨씬 쉬워진다.
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();
}
예제
provider의 get() 호출시 : 스프링 컨테이너를 통해 해당 빈을 찾아서 반환(DL)하게 된다.
provider의 get()이 Dependency Lookup 기능을 잘 실현한다.
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
테스트
@Test
void prototypeFind(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
ObjectProvider , JSR330 Provider는 위 프로토타입 관련 예시 뿐만 아니라,
DL이 필요한 경우 언제든지 사용할 수 있으므로 간단히 기억해두자.
'web +a' 카테고리의 다른 글
웹 스코프 | request scope (0) | 2022.07.15 |
---|---|
테스트 주도 개발 (TDD) (0) | 2022.07.14 |
스프링빈 생명주기 콜백 메서드 (0) | 2022.07.14 |
자동/수동 빈에 대한 올바른 선택 (0) | 2022.07.14 |
annotation 직접 만들기 (@Qualifier 관련) (0) | 2022.07.13 |