빈 스코프에 대해서 알아보자.
오늘은 9강을 마저 듣고. HTTP 통신 책을 공부할 생각이다.
내가 행복했으면 좋겠다.🍀
📌 빈 스코프란
📌
지금까지는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가
종료될 때까지 유지되는 것을 보았는데 이는 "싱글톤 스코프"의 특성이다.
기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
➡️ 빈 생성 , 의존관계 주입, 초기화까지만 보장!
스프링 웹과 관련한 기능이 들어가야 사용할 수 있다.
request
: 웹(고객) 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
session
: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
application
: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
@Scope( )
를 통해 적으면 된다.@Scope("prototype")
@Component
public class HelloBean {}
2. 수동 등록 방법
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean(); //수동 빈 등록 방법이라서 생성자 사용
}
예제코드
public class SingletonTest {
@Test
void singletonBeanFind() {
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);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton") // 디폴트이긴 함
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
private void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
💻 실행화면
@PostConstruct
, @PreDestroy
애노테이션을 통해 초기화와 소멸 메소드를 지정해줌SingletonBean.init
singletonBean1 = hellospring.demo.scope.SingletonTest$SingletonBean@6f53b8a
singletonBean2 = hellospring.demo.scope.SingletonTest$SingletonBean@6f53b8a
SingletonBean.destroy
스프링 컨테이너는 프로토타입 빈에 대해서 다음을 처리한다.
→ 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
→ 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
👉 그래서 @PreDestroy
같은 종료 메서드가 호출되지 않는다.
public class PrototypeTest {
@Test
void prototypeBaenFing() {
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
private void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
💻 실행결과
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean.init
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hellospring.demo.scope.PrototypeTest$PrototypeBean@4d1bf319
prototypeBean2 = hellospring.demo.scope.PrototypeTest$PrototypeBean@6f53b8a
싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.
프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다.
싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만,프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하고, 더는 관리하지 않는다.
따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy 같은 종료 메서드가 전혀 실행되지 않는다.
➡️ 직접 종료메서드를 호출해야한다.
prototypeBean1.destroy();
prototypeBean2.destroy();
프로토 타입은 자주 사용하지 않고, 싱글톤을 주로 사용하게 되는데 그러면,
싱글톤과 함께 쓰게 된다. 그럴때 발생하는 문제점이 있다. 문제점을 알아보자.
싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않는다.
➡️ 결과적으로 프로토타입 빈(x01)의 count
는 1이 된다.
count
필드를 +1 한다.➡️ 결과적으로 프로토타입 빈(x02)의 count는 1이 된다
해당 예제코드
public class SingletonWithPrototypeTest1 {
@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");
}
}
}
<싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예>
clientBean
이 프로토타입 빈인 prototypeBean
을 가진다.clientBean
은 내부 필드에 프로토타입 빈 prototypeBean
의 참조값을 가진다.클라이언트 1이 스프링 컨테이너에 clientBean
을 요청한다.
클라이언트 1이 clientBean.logic()
를 호출한다.
➡️ prototypeBean의 addCount()
실행
➡️ +1된 프로토타입 빈의 count
반환
✔️ 실행결과
= 1
클라이언트 2이 스프링 컨테이너에 clientBean
을 요청한다.
✔️ clientBean
은 싱글톤으로 스프링 컨테이너는 아까와 같은 동일한 인스턴스를 반환한다.
⭐ clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
클라이언트 2이 clientBean.logic()
를 호출한다.
➡️ prototypeBean의 addCount()
실행
➡️ +1된 프로토타입 빈의 count
반환
✔️ 실행결과
≠ 1
→ 실제 실행결과 = 1
해당 예제코드
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean client1 = ac.getBean(ClientBean.class);
int count1 = client1.logic();
assertThat(count1).isEqualTo(1);
ClientBean client2 = ac.getBean(ClientBean.class);
int count2 = client2.logic();
assertThat(count2).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
💻 실행화면
count1 = 1
count2 = 2
싱글톤빈에서 등록을 할 때 의존관계를 주입한다.
✔️ 이때 프로토타입 빈이 생성이 되고, 할당이 된다.
➡️ 이후 클라이언트 1,2가 logic
을 호출할 때 할당되었던 동일한 프로토타입빈을 가진다.
⭐주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!
여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다. 예를 들어서 clientA, clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.
사용할 때 마다 새로 생성해서 사용하고 싶었는데...
예제코드
@Scope("singleton")
static class ClientBean {
@Autowired
ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
➡️ 그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면,
스프링 컨테이너에 종속적인 코드가 되고,단위 테스트도 어려워진다.
🤔 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 된다.
⚒️ ObjectProvider
는 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다.
예제코드
public class SingletonWithPrototypeTest1 {
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean client1 = ac.getBean(ClientBean.class);
int count1 = client1.logic();
assertThat(count1).isEqualTo(1);
System.out.println("count1 = " + count1);
ClientBean client2 = ac.getBean(ClientBean.class);
int count2 = client2.logic();
System.out.println("count2 = " + count2);
assertThat(count2).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeansProvider;
//⚒️ ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는
// DL 서비스를 제공한다.
public int logic() {
PrototypeBean prototypeBean = prototypeBeansProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@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");
}
}
}
💻 실행코드
ObjectProvider 의 getObject()
를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)PrototypeBean.init :
hellospring.demo.scope.SingletonWithPrototypeTest1$PrototypeBean@6b09fb41
count1 = 1
PrototypeBean.init :
hellospring.demo.scope.SingletonWithPrototypeTest1$PrototypeBean@23202fce
count2 = 1
ObjectFactory
vsObjectProvider
ObjectFactory
가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider
가 만들어졌다.
javax.inject.Provider 라는 JSR-330
자바 표준을 사용하는 방법이다.스프링부트 3.0
javax.inject:javax.inject:1
스프링부트 3.0
jakarta.inject:jakarta.inject-api:2.0.1
스프링 부트 3.0
jakarta.inject.Provider
public class SingletonWithPrototypeTest {
@Test
void singletonClientUsePrototype(){
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean client1 = ac.getBean(ClientBean.class);
int count1 = client1.logic();
assertThat(count1).isEqualTo(1);
System.out.println("count1 = " + count1);
ClientBean client2 = ac.getBean(ClientBean.class);
int count2 = client2.logic();
System.out.println("count2 = " + count2);
assertThat(count2).isEqualTo(1);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeansProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeansProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@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");
}
}
}
💻 실행화면
provider
의 get()
을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)Provider
는 지금 딱 필요한 DL 정도의 기능만 제공한다.PrototypeBean.init : hellospring.demo.scope.SingletonWithPrototypeTest$PrototypeBean@479cbee5
count1 = 1
PrototypeBean.init : hellospring.demo.scope.SingletonWithPrototypeTest$PrototypeBean@1c852c0f
count2 = 1
매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
ObjectProvider
, JSR330 Provider
의 사용ObjectProvider
, JSR330 Provider
등은 프로토타입 뿐만 아니라 DL이 필요한 경우에 언제든지 사용할수 있다.
➕ ObjectProvider
는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별
도의 의존관계 추가가 필요 없기 때문에 사용이 더 편리하다.
📌 좋은 코드란?
→ 의도대로 동작하는 코드?