개발자로 후회없는 삶 살기
spring PART.빈 스코프 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.inflearn.com/roadmaps/373
본론
- 빈 스코프
스프링 컨테이너가 생성이 될 때 빈 들도 생성이 되고 사라지면 사라진다고 했었는데 그 이유가 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문입니다. 스코프는 빈이 언제부터 언제까지 살아있나를 의미합니다. 싱글톤 스코프가 기본인데 즉 아까는 빈 들이 생성되고 등록되고 DI되고 종료되는게 컨테이너와 동일하게 일어났는데 그걸 다르게 바꿀 수 있는 것입니다.
-> 스프링은 다음과 같은 다양한 스코프를 지원합니다.
1. 싱글톤
기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 생명주기가 긴 스코프입니다.
2. 프로토타입
이름 그대로 대충만들고 버리는 스코프로 스프링 컨테이너가 프로토타입 빈을 만들어는 줍니다. 그리고 DI까지 해주고 초기화 메서드(스코프에서 초기화는 초기화 메서드이다.)까지 불러주고 그게 끝으로 컨테이너가 관리하지 않습니다.
3. 웹 관련 스코프
1) request : 웹 요청이 들어오고 요청이 나갈 때까지 생존 범위를 가지는 스코프입니다. 요청이 들어올 때 생성이 되어서 요청이 나갈 때 destory가 되는 스코프입니다.
2) session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프입니다. 보통 로그인에 씁니다.
3) application : 웹에 서블릿 컨텍스트라는 범위가 있는데 그때동안 유지되는 스코프입니다.
- 프로토타입
지금까지 싱글톤 스코프를 썼으니 싱글톤과 비교하면서 프로토타입을 설명해보자 싱글톤 스코프의 경우 빈을 조회한면 싱글톤 컨테이너라서 항상 같은 인스턴스의 스프링 빈을 반환합니다.
반면 프로토타입은 항상 새로운 객체를 만듭니다. 클라 A가 빈을 요청하면 스프링 컨테이너가 그제서야 빈 객체를 생성하고 필요한 DI를 합니다.(싱글톤에서는 컨테이너가 생성되면서 빈도 같이 생성했습니다.)
그리고 클라 A에게 던지고 더이상 컨테이너가 관리하지 않습니다. B가 해도 C가 해도 항상 새로운 빈을 생성하고 주입하고 던지고 끝냅니다. 프로토타입 빈을 관리할 책임은 빈을 받은 클라이언트에게 있습니다. 따라서 destroy 메서드가 호출되지 않습니다.
-> 코드
@Test
void singletonTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean3 = ac.getBean(SingletonBean.class);
System.out.println(singletonBean1);
System.out.println(singletonBean3);
ac.close();
}
싱글톤 스코프 빈을 만든다. 컨테이너에 빈을 직접 등록하면 빈이 하나만 등록이 됩니다. getBean으로 조회하면 두개의 꺼낸 빈이 같은 객체입니다.
결과를 보면 싱글톤으로 같은 객체를 공유하고 컨테이너가 종료할 때 빈도 같이 소멸하는 것을 destory로 알 수 있습니다.
// 싱글톤
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("init");
}
@PreDestroy
public void close() {
System.out.println("destroy");
}
}
// 프로토타입
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("init");
}
@PreDestroy
public void close() {
System.out.println("destroy");
}
}
프로토 타입 빈을 만듭니다. @scope("prototype")이라고 하고 같은 코드를 실행하면 다른 객체가 나오고 destory가 나오지 않습니다.
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
System.out.println("find proto1");
PrototypeBean PrototypeBean1 = ac.getBean(PrototypeBean.class);
System.out.println("find proto2");
PrototypeBean PrototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println(PrototypeBean1);
System.out.println(PrototypeBean2);
ac.close();
}
그리고 중요한게 컨테이너가 생길 때 빈이 생기는게 아니고 클라가 빈을 요청할 때 생성된다고 했습니다. 따라서 결과를 보면 원래 항상 init이 먼저 나왔었는데 init이 나중에 나옵니다.
=> 프로토 타입을 싱글톤과 함께 쓰기
프로토를 사용하면 항상 새로운 객체를 만들어서 반환한다고 했는데 싱글톤과 함께 쓰면 의도한대로 동작하지 않습니다.
1. 프로토타입 직접 요청
클라가 스프링 컨테이너에 프로토타입 빈을 호출(여기의 호출은 getBean으로 조회할 때입니다.)했고 그걸 그제서야 생성해서 반환한다고 했습니다.
@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("init" + this);
}
@PreDestroy
public void close() {
System.out.println("destroy");
}
}
빈 안에 count가 있고 클라가 addCount() 메서드를 호출하면 1씩 증가한다고 해보자 다른 클라가 빈을 요청하면 새로 생성한 거니깐 count는 다 1, 1이어야합니다.
@Test
void sinwithProTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
prototypeBean1.addCount();
prototypeBean2.addCount();
Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1);
Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
코드로 실행해보면 count가 각각 1씩 증가해서 테스트에 성공합니다.
2. 싱글톤 빈에서 프로토타입 빈 사용
실무에서는 대부분 싱글톤 빈을 사용하고 필요시에 싱글톤 빈이 프로토를 씁니다. 근데 이 둘을 같이 쓰면 문제가 있습니다. cliBean이라는 싱글톤 빈이 있고 프로토 빈을 주입 받아서 사용하는 경우를 보겠습니다.
1) 그러면 클라 빈이 생성될 때 싱글톤이니 생성과 DI가 같이 돼서 프로토를 가지고 있습니다. cliBean이 싱글톤이라서 의존 관계 자동 주입을 사용하면 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청합니다.
2) 그러면 스프링 컨테이너는 프로토 빈을 생성해서 cliBean에 반환합니다. 그러면 프로토 빈은 컨테이너가 더 이상 관리를 하지 않고 싱글톤 빈이 관리를 하게 됩니다.
-> 상황 부여
이 상황에서 클라가 cliBean의 logic을 호출합니다. 이 로직에서 프로토의 addcount를 한다. 프로토의 count가 1이됩니다.
> 클라 B가 cliBean을 받아서 addcount를 하면 프로토의 2를 받게됩니다. cliBean이 내부에 가지고 있는 Bean은 이미 과거에 주입이 끝난 빈이라서 프로토타입 빈이라도 사용할 때마다 새로 생성되는 것이 아니라 생성된 빈을 그대로 사용합니다.
-> 코드 작성
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
싱글톤 cliBean을 작성하고 새로운 테스트 클래스를 만듭니다.cliBean은 프로토타입 빈을 주입 받습니다. logic은 프로토타입의 addcount를 호출합니다.
@Test
void singleUsePro() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int cnt1 = clientBean1.logic();
assertThat(cnt1).isEqualTo(1);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int cnt2 = clientBean2.logic();
assertThat(cnt2).isEqualTo(2);
}
컨테이너는 이제 싱글톤과 프로토 빈을 둘 다 지정받습니다. 클라1이 cliBean을 호출하면 프로토의 cnt가 1이되고 클라 2가 호출하면 2가 됩니다. 컨테이너의 관리를 떠난 것입니다.
> 하지만 우리가 프로토타입을 쓰는 이유는 호출할 때 마다 새로 만들려고 하는 것입니다. 이렇게 되면 싱글톤을 쓰지 프로토타입을 쓸 이유가 없습니다.
- 문제 해결
어떻게 하면 싱글톤과 프로토타입을 같이 사용할 때 새로운 프로토 빈을 생성할 수 있을까요?
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
싱글톤 빈의 로직을 고쳐서 ApplicationContext를 cliBean에 선언하고 getBean으로 하면 로직을 실행할 때마다 컨테이너가 getBean으로 프로토 빈을 호출하는 것이기 때문에 프로토만 사용할 때 처럼 매번 새로 생성할 수 있습니다.
> 의존 관계를 외부에서 주입 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup 의존관계 조회라고 합니다. 우리는 이런 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱 DL 정도의 기능만 제공하는 무언가가 필요합니다. 하지만 위 코드로 하면 스프링 컨테이너에 종속적인 코드가 됩니다. 컨테이너는 정말 다양한 기능을 제공하는데 그 중 DL만 해주는게 필요합니다.
- ObjectProvider
컨테이너를 다 쓰기에는 그러니 컨테이너에게 대신 찾아서 찾아달라고 하는 게 있습니다.
// 전
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
// 후
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
-> 코드
필드 주입으로(테스트니깐) 오프를 받고 프로토를 제네릭합니다.(오프는 스프링이 컨테이너에 알아서 등록하는 것이라 등록하지 않아도 주입해 사용할 수 있습니다.) 프로바이더가 대신 찾아주는 것입니다.
> getObject를 호출하면 그때서야 스프링 컨테이너에서 프로토를 찾아서 반환해주는 것입니다. 싱글톤 빈을 생성할 때 프로토 빈을 바로 주입받지 않고 컨테이너에게 프로토 빈을 찾아달라는 대리자를 주입하는 것입니다. 코드를 컨테이너에 종속적으로 하는게 아니라 찾아주는 기능만 구현한 것입니다.
이렇게 하면 프로토가 매번 새로 생성됩니다. 과거에는 ObjectFactory라는게 있었는데 그 자식으로 편의 기능을 제공하는게 오프입니다. 핵심 컨셉은 대신 조회해주는 대리자입니다. 프로토타입에 국한된 것은 아닙니다. 프로토 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있습니다.
※ 하지만 스프링 코드를 그대로 사용하는 것이라서 스프링에 의존적입니다.
- JSR-330 Provider
스프링에 의존하지 않는 새로운 기능으로 자바 표준을 정했습니다.
인터페이스를 보면 get만 있습니다.
@Scope("singleton")
static class ClientBean {
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
기존에 있던 오프를 Provider로 바꾸고 get으로 바꾸면 끝납니다. 장점은 심플하고 단점도 심플합니다. 딱 DL 정도의 기능만 제공하고 자바 표준이라서 스프링이 아닌 다른 컨테이너에서도 사용할 수 있습니다.
-> 정리
프로토를 언제 사용할까요? 프로토는 매번 호출할 때마다 새로운 객체가 필요하면 사용하면 됩니다. 근데 막상 실무에서 웹 어플을 개발해보면 싱글톤으로 대부분 해결할 수 있기에 프로토를 직접적으로 사용하는 일은 매우 드뭅니다.
'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글
spring PART.서블릿 프로그래밍 개요 (0) | 2023.04.07 |
---|---|
spring PART.request scope (0) | 2023.04.02 |
spring PART.의존관계 자동 주입 (0) | 2023.03.31 |
spring PART.컴포넌트 스캔 (0) | 2023.03.29 |
spring PART.스프링 컨테이너 계층, 싱글톤 컨테이너 (0) | 2023.03.29 |