빈 스코프란?
스프링 빈은 스프링 컨테이너 시작 때 생성되어 컨테이너 종료까지 유지된다.
이는 스프링 빈이 기본적으로 싱글톤 스코프이기 때문이다.
스코프는 빈이 존재할 수 있는 범위를 뜻한다.
스프링은 다음과 같은 스코프를 지원한다.
- 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료시 까지 유지되는 가장 넓은 범위의 스코프이다.
- 프로트타입 : 스프링 컨테이너는 프로트타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
웹 관련 스코프
- request : 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
- session : 세션이 생성되고 종료될 떄 까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프이다.
- 등록 예제*
컴포넌트스캔
@Scope("prototype")
@Component
public class Bean {
...
}
*수동 등록*
@Scope("prototype")
@Component
public class Bean {
...
}
## 프로토 타입 스코프
싱글톤 스코프의 빈 조회 시 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
반면 프로토타입 스코프의 빈을 조회하면 항상 새로운 인스턴스를 생성해서 반환한다.
(이전에 싱글톤 빈에 대해 다룰 때 언급한 적 있다)
라이프사이클
- 프로토타입 빈을 요청한다.
- 스프링 컨테이너는 빈을 생성하고 의존관계를 주입한다.
- 만들어진 빈을 클라이언트에 반환한다.
- 이후 같은 요청이 들어오면 위 과정을 다시하여 프로토타입 빈을 생성해서 반환한다.
정리
핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리해주는 것
클라이언트 반환 이후에는 생성된 빈을 관리하지 않는다.
그래서 종료 메서드 (@PreDestory)를 사용할 수 없다.
(인스턴스를 받은 이후에는 클라이언트가 책임을 지는 것)
예제
싱글톤으로 만든 빈의 속성비교
@Test
void singletonBeanFind() {
ApplicationContext 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);
}
@Scope("singleton")
static class SingletonBean{
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destory() {
System.out.println("SingletonBean.destory");
}
}
출력값
SingletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@c0c2f8d
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@c0c2f8d
09:39:07.950 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@b7f23d9, started on Fri Jul 16 09:39:07 KST 2021
SingletonBean.destory
라이프 사이클 콜백도 잘 실행 되었고 똑같은 객체임을 알 수 있다.
프로토 타입 스코프로 만든 빈의 속성 비교
@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();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destory() {
System.out.println("PrototypeBean.destory");
}
}
출력값
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.ProtoTypeTest$PrototypeBean@c0c2f8d
prototypeBean2 = hello.core.scope.ProtoTypeTest$PrototypeBean@305b7c14
09:50:09.103 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@b7f23d9, started on Fri Jul 16 09:50:08 KST 2021
프로토타입 빈의 객체 값이 서로 다르다.
싱글톤 빈과 차이점
- 싱글톤 빈과 차이점으로 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행되지만 프로토타입 스코프 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드가 실행된다.
- 프로토타입 빈을 2번 조회했으므로, 다른 스프링 빈이 생성되고 초기화가 2번 생성되었다.
- 종료메서드가 실행되지 않는다.
prototypeBean1.destory();
prototypeBean2.destory();
만약 프로토타입 빈의 종료메서드 호출이 필요하다면 위 처럼 직접 호출해야 한다.
(스프링 컨테이너 손에서 떠나갔기 때문)
프로토타입 빈의 특징 정리
- 스프링 컨테이너 요청할 떄 마다 새로 생성한다.
- 스프링 컨테이너는 생성, DI, INIT까지만 관여한다.
- 종료 메서드가 실행되지 않는다.
- 고로 클라이언트가 관리해야 하며 종료 메서드도 직접 실행해야함.
프로토타입 스코프와 싱글톤 빈을 함께 사용할 때 문제점
이 프로토타입 스코프 빈과 싱글톤 빈을 함께 사용하면 의도한 대로 동작하지 않으므로 주의하여야 한다.
@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();
System.out.println("prototypeBean1.getCount() = " + prototypeBean1.getCount());
System.out.println("prototypeBean2.getCount() = " + prototypeBean2.getCount());
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@Scope("prototype")
static class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
}
만약 prototype으로 생성한 카운트 변수 빈을 각각 addCount를 적용시키면 아래와 같은 출력결과를 얻을것이다.
prototypeBean1.getCount() = 1
prototypeBean2.getCount() = 1
싱글톤 빈에서 프로트타입 빈이 사용된다면?
이번에는 clientbean 이라는 싱글톤 빈에 의존관계 주입으로 프로토타입 빈을 주입받아서 사용하는 경우를 만들어보자.
clientBean은 싱글톤이므로, 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.- 스프링 컨테이너는 프로토타입 빈을 생성해서
cleintBean에 반환한다. 프로토타입 빈의 count 필드 값은 0이다. - 이제
clientBean은 프로토타입 빈을 내부 필드에서 보관한다. (참조값을 보관하는 것)
여기서 만약 두명의 클라이언트 A, B가 각각 prototypeBean.addCount를 요청하게 되면
스프링 컨테이너는 앞서 요청한 클라이언트가 앞서 만들어둔 싱글톤 빈 내부의 porototype 인스턴스를 사용하게 되므로 prototypebean의 count는 각 각 1이 되는 것이 아닌 2가 되버린다.
예제코드
@Test
void singletonClientUseProtoType() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
System.out.println("count1 = " + count1);
assertThat(count1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
System.out.println("count2 = " + count2);
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();
return prototypeBean.getCount();
}
}
출력값
count1 = 1
count2 = 2
ClientBean 클래스의 의존클래스인 prototypeBean은 생성되는 순간 clientBean 내부에 주입되어 관리 되므로 본래 속성이 무시되는 것이다.
개발자가 사용할 때 마다 새로 생성해서 사용하는 동작을 원할 것이다.
해결방법
1. 스프링 컨테이너에 직접 요청하기
@Scope("singleton")
static class ClientBean {
// private final PrototypeBean prototypeBean; //생성 시점에 주입
@Autowired
ApplicationContext applicationContext;
// public ClientBean(PrototypeBean prototypeBean) {
// this.prototypeBean = prototypeBean;
// }
public int logic() {
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
의존관계 주입와 객체생성 생성자를 통해서가 아닌 메소드를 호출할 때 발생시키게 코드를 수정해보고 다시 실행해보았다.
출력값
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@5f9b2141
count1 = 1
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@247d8ae
count2 = 1
서로 다른 객체로 생성되어 의도한대로 동작되었다.
당장은 해결되었지만 스프링 컨테이너 호출부가 logic 메소드 안에 들어가버리면서 코드가 복잡해진다.
- 의존관계를 외부에서 주입받는(DI) 상황이 아니라 직접 필요한 의존관계를 찾는 상황이 되었다. 이를 Dependency Lookup (DL) 의존관계 조회(탐색) 이라고 한다.
- 스프링 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트 또한 어려워짐
- 지금 필요한 기능은 프로타입 빈을 컨테이너에서 대신 찾아주는 기능이다.
이런 문제도 스프링은 준비를 해두었다.
2. ObjectFactory, ObjectProvider
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
ObjectProvider에 제네릭 타입을 지정해주고 getObject를 사용하면 그 때 prototypeBean을 반환해준다.
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@33990a0c
count1 = 1
PrototypeBean.inithello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@50b5ac82
count2 = 1
prototype 객체가 별개로 생성되어 호출된 결과를 얻을 수 있다.
ObjectProvider는 ObjectFactory를 구현한 클래스로 ObjectFactory는 getObject하나만 지원하지만 Provider는 조금 더 다양한 기능을 제공하고 있다.
ObjectProvider의getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 실행한다. (DL)- 단순한 기능이라 단위테스트나 mock코드 만들기 훨씬 쉽다.
특징
- ObjectFactory : 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많고, 별도의 라이브러리가 필요 없다.
3. JSR-330 Provider
마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.
이 방법을 사용하기 위해서는 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
예제 코드
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
provider.get() 을 통해 객체가 대입되며 2번 예제와 똑같은 결과를 출력한다.
특징
get()메서드 하나로 기능이 매우 단순하다.- 별도 라이브러리를 받아야 한다. (javax:inject)
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에도 사용할 수 있다.
정리
- 프로토타입 빈은 매번 사용 시 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용하면 된다.
실무에서는 대부분 싱글톤 빈으로 해결이 되므로 프로토타입 빈을 사용하는 일은 드물다. ObjectProvider,JSR03 Provider등은 프로토 타입이 아니여도 언제든 사용할 수 있다.- 스프링 제공 기능이나 자바 제공 기능이나 선택하여 사용하면 된다.
'JAVA > Spring' 카테고리의 다른 글
| [Spring] 스프링 트랜잭션과 동작방식에 대한 간략한 기록 (0) | 2022.06.20 |
|---|---|
| [Spring] 웹 스코프 (0) | 2021.07.16 |
| [Spring] 빈 생명주기의 콜백 (0) | 2021.07.15 |
| [Spring]컴포넌트 스캔과 의존관계 자동 주입 (Component Scan, Autowired) (0) | 2021.07.14 |
| [Spring] 스프링과 싱글톤 (0) | 2021.07.13 |