
김영한 강사님의 스프링 핵심 원리의 강의 내용과 자료를 이용했음을 밝힙니다.
빈 스코프 -> 우리가 배운 Bean과 영어 Scope의 합친 말이다.

여기서 2번의 의미를 여기에 적용하면 빈의 범위, 좀 더 자세하게 말해서 빈이 존재할 수 있는 범위를 말한다.
일종의 빈의 수명이라고 생각하면 되겠다.
Q. 그런데 이상하네 스프링 빈은 스프링 컨테이너랑 같이 생성되서 컨테이너가 종료될 때까지 유지되는거 아닌가?
그건 싱글톤 스코프이다.
기본적으로 스프링 빈은 싱글톤 스코프로 생성되기 때문에 그렇게 알고 있는 것이다.
Q. 그러면 다른 스코프도 존재하는건가?
그렇다. 스프링은 다양한 스코프를 지원한다.
그 중에
- 싱클톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입: 스프링 컨테이너가 빈의 생성과 의존관계 주입까지만 관여하고 그 이후는 관리하지 않는 짧은 범위의 스코프이다.
- request: 웹 관련 스코프 중 하나로 웹 요청(고객)이 들어오고 나갈 때까지 유지되는 스코프
이 3가지를 알아볼 것이다. (이 외에도 여러 스코프가 존재한다.)
일단 프로토타입 빈을 만드는 방법 먼저 알아보자면
단순히 빈에 @Scope("Prototype")을 붙여주면 된다.
@Scope("Prototype")
@Bean
PrototypeBean Hello(){..}
그럼 이제 프로토타입 빈이 뭔지 알아보자.

이게 일반적으로 우리가 아는 싱글톤 빈이다.
그리고 프로토타입 빈을 보자.

이제 차이점을 알아보자
싱글톤 빈은 클라이언트들이 요청들을 하면 하나의 객체를 만들어서 싱글톤 패턴으로 즉 하나의 객체를 공유해서 제공해주었다.
하지만 프로토타입 빈을 보면 클라이언트가 요청할 때마다 새로운 빈 객체를 만드는 것을 알 수 있다.
하지만 여기에 한 가지 더 놀라운 프로토타입의 특징이 있다.
위에서 말해서 알수도 있는데 바로 스프링 컨테이너가 객체 생성->의존관계 주입-> 초기화 후 클라이언트한테 던지고(반환하고) 더이상 관리하지 않는다.
그러니 그 이후 관리는 클라이언트한테 책임이 있으니 종료 콜백 메서드인 @PreDestroy도 호출되지 않는다.
한번 test를 해보자
@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();
prototypeBean1.destroy();
prototypeBean2.destroy();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
실행결과
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing
이런 식으로 Prototype 빈을 만들고 2개를 요청해서 보면 다르게 나오고
ac.close()를 통해서 destroy() 즉 종료 콜백 함수를 호출했지만 호출되지 않는 것을 확인할 수 있다.
만약에 종료를 할려면 prototypeBean1.destroy() 이런식으로 직접 호출해야 한다.
===============================================================
그리고 좀 뜬금없지만 중요한 질문
Q. ProtytypeBean에는 @Bean 혹은 @Component도 없는데 어떻게 빈으로 등록되는 것인가?
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(PrototypeBean.class);
이런 식으로 클래스를 파라미터로 주면 이 클래스를 컴포넌트로 등록한다.
===============================================================
일단 프로토타입만 사용했을 때를 보자
@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);
}
@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");
}
}
이 테스트는 프로토타입 스코프 빈 객체를 2개를 만든 다음에 각각의 객체에 빈 객체에 있는 변수(count)값을 +1하고 확인하는 test이다.
당연히 프로토 타입이기에 prototypeBean1과 prototypeBean2는 아예 다른 객체이고 그러므로 prototypeBean1, prototypeBean2의 count는 각각 1이다.
그러면 싱글톤 빈에서 프로토타입 빈을 사용하는 경우를 보자.
@Scope("singleton")
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" + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
이런 식으로 싱글톤 빈에서 프로토타입 빈을 변수로 가지고 있고 함수 logic()을 통해 이 프로토타입 빈의 변수인 count의 값을 +1한 뒤 반환한다고 해보자.
아까 위와 같은 test를 진행해보자
@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(1);
}
자 이 test는 통과할까?

왜 그럴까?
싱글톤 빈(ClientBean)이 내부에 가지고 있는 프로토타입 빈은 싱글톤 빈이 의존관계 자동주입을 하는 시점에 요청되고 생성된다.
이 시점에 미리 만들어지고 나서 logic에서 호출할 때마다 이렇게 미리 만들어진 프로토타입 빈이 사용되기에 count2는 count1에 이어서 한번 더 +1이 된 것이라 값이 2가 된다.
그렇군 근데 여기서 의문이 든다.
굳이 프로토타입 빈을 사용했다면, 요청할 때마다 새로 생성되는 프로토타입 빈의 성질을 이용하는 것이 맞지 않을까? 아니면 굳이 왜 프로토타입을?

자 그러면 위의 문제를 어떻게 해결할지 알아보자.
우선 단순하게 생각을 하면 logic() 메서드를 사용할 때마다 프로토타입 빈을 요청하는 것이다.
@Scope("singleton")
static class ClientBean{
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
그래서 애플리컨테이션처럼 수많은 기능을 지원해주는 것이 아니라 DL(직접 필요한 의존관계를 찾는 것)정도의 기능만 제공하는 무언가가 필요하다.
그 무언가가 2개가 있다.
- ObjectFactory, ObjectProvider
- JSR-330 Provider
위에서 말한 DL 기능을 가진게 ObjectFactory이고 최근에는 편의기능을 더 추가해서 ObjectFactory를 상속한 ObjectProvider를 사용한다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
이런식으로 싱글톤 빈을 수정하면 prototypeBeanProvider.getObject()를 통해서 항상 새로운 프로토타입 빈이 생성된다.
딱 필요한 정도의 DL 기능을 제공하지만 스프링에 의존한다는 단점이 존재한다.
이 방법은 javax.inject.Provider이라는 JSR-330 자바 표준을 사용하는 방법이다.
눈치 빠른 사람들은 이미 알았을 것이다.
"javax"
자바 표준 즉 스프링에 의존하지 않는다는 것이다. -> 그러니 ObjectProvider보다 좋겠지?
이 방법을 사용할려면
스프링부트 3.0 미만은 javax.inject:javax.inject:1
스프링부트 3.0 이상은 jakarta.inject:jakarta.inject-api:2.0.1
를 gradle에 추가해주면 된다.
@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;
}
}
이렇게 사용하면 prototypeBeanProvider.get()을 통해서 항상 새로운 프로토타입 빈이 생성된다.
provider의 get()을 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다 -> DL
장점
- get() 메서드 하나로 기능이 매우 단순하다.
- 딱 DL정도의 기능만 제공
- 자바 표준이랑 스프링이 아닌 다른 컨테이너에서도 사용가능
단점
- 별도의 라이브러리 필요
Q. ObjectProvider vs JSR-330 Provider
스프링이 아닌 다른 컨테이너에서 사용할려면 JSR-330을 사용하고 그게 아니면 크게 상관은 없는 것 같다.
하지만 그래도 개인적으로 사용할 수 있는 범위가 넓은 JSR-330을 사용하는 것이 좋지 않나 싶다.
Q. 스프링 vs 자바표준
위의 ObjectProvider vs JSR-330 Provider 처럼 스프링의 기능과 자바표준의 기능이 겹칠 때가 존재한다.
그럴땐 어느 걸 사용해야 할까?
첫 번째로는 기능이 더 나은 것을 선택하고 그 외에는 대부분 스프링이 더 다양하고 편리한 기능을 제공하기에 스프링을 선택하면 되겠다.