김영한님의 Spring 핵심원리 강의를 듣고 정리하는 글입니다.
1. 빈 스코프란(Bean Scope)?
빈 스코프는 말 그대로 빈이 존재할 수 있는 범위를 뜻한다. 스프링에서는 다양한 빈 스코프를 지원한다.
2. 빈 스코프 종류
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작부터 종료까지 유지되는 스코프
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하는 소코프
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
- session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application: 웹의 서플릿 컨텍스트와 같은 범위로 유지되는 스코프
스코프를 설정하는 방법이다.
컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {...}
수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
3. 싱글톤 스코프
Scope("singleton") // 생략가능(기본 등록시 singleton 빈으로 등록)
@Component
public class MemberServiceImpl implements MemberService {...}
- 빈을 등록할 때 아무런 설정을 하지 않으면 기본적으로 싱글톤 스코프를 갖는다.
- 싱글톤 스코프는 스프링 컨테이너의 시작부터 종료까지 유지되는 스코프이다.
위 그림을 보면 모든 클라이언트 요청을 같은 객체 인스턴스의 스프링 빈을 반환하는 것을 확인할 수 있다.
싱글톤 스코프 테스트 예제
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
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
실행결과
SingletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@1cf56a1c
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@1cf56a1c
SingletonBean.destroy
- 실행결과를 보면 같은 getBean() 요청을 할 때마다 같은 인스턴스를 반환하는 것을 확인할 수 있다.
- init(), destory()가 모두 호출된 것을 확인할 수 있다. (스프링 컨테이너가 빈의 생성부터 소멸까지 전체 라이플 사이클을 관리하는 것을 확인)
4. 프로토타입 스코프
- 프로토타입 스코프는 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여
프로토타입 생성 및 요청과정
- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
- 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환
- 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환
핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.
5. 싱글톤과 프로토타입을 함께 사용할 때 문제점
스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않을 수 있으므로 이에 대한 원인을 알아보자.
왜 제대로 동작할지 않을까?
정리부터 하자면 싱글톤 스코프의 빈은 스프링 컨테이너 생성시점에 생성 → 의존관계 주입 → 초기화 작업을 수행하기 때문에 최초 의존관계 주입 시점에 하나의 빈 인스턴스 정보를 가지기 때문이다.
예시코드를 살펴보자.
public class SingletonWithPrototypeTest1 {
@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);
}
@Scope("singleton")
static class ClientBean {
@Autowired
private 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("Prototype.destroy");
}
}
}
예상된 결과는 1인데 2가 출력된 것을 확인할 수 있다.
왜 예상된 결과와 다른값이 나올까?
과정을 보면,
- 의존관계 주입 시점에 프로토타입 빈을 스프링 컨테이너에 요청하게 된다.
- 스프링 컨테이너는 프로토타입 빈을 생성하고, 클라이언트 빈에 반환한다. 이 때 count(필드)는 초기화 시점 상태로 0을 갖는다.
- clientBean은 프로토타입 빈의 참조값을 보관한다.
- 프로토타입 빈의 참조값을 가지기 때문에 클라이언트A가 호출할 때 1을 증가 시켜서 count는 1이 되고, 클라이언트B가 호출할 때 또 1을 증가시켜 count는 2를 갖는다.
그럼 싱글톤과 프로토타입을 같이 쓸 때 매번 새로운 프로토타입 주입 받는 방법은 없을까?
6. 싱글톤과 프로토타입을 함께 사용할 때 문제점 해결 방법
a. ObjectFactory로 문제 해결
@Scope("singleton")
static class ClientBean {
@Autowired
private ObjectFactory<PrototypeBean> prototypeBeanFactory;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanFactory.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
b. ObjectProvider로 문제 해결
@Scope("singleton")
static class ClientBean {
// **ObjectProvider 사용**
@Autowired
**private ObjectProvider<PrototypeBean> prototypeBeanProvider;**
public int logic() {
**PrototypeBean prototypeBean = prototypeBeanProvider.getObject();**
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- 실행결과를 보면 새로운 프로토타입 빈을 생성되는 것을 확인할 수 있다.
- ObjectFactory, ObjectProvider의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- ObjectFactory가 ObjectProvider 에 비해 기능이 단순하다. (ObjectFactory에서 여러 기능을 추가한 게 ObjectProvider 이다.)
- ObjectFactory, ObjectProvider 스프링에 의존적이다.
c. JSR-330 Provider 문제 해결
- jakarta.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법이다.
- 스프링 부트 3.0 이하는 javax.inject.Provider를 사용한다.
- 자바 표준이지만 사용 전에 라이브러리 추가해야 한다.
- 스프링 부트 3.0 이상: jakarta.inject:jakarta.inject-api:2.0.1 라이브러리를 gradle에 추가
- 스프링 부트 3.0 미만: javax.inject:javax.inject:1 라이브러리를 gradle에 추가
@Scope("singleton")
static class ClientBean {
// ObjectProvider 사용
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- 실행결과를 보면 새로운 프로토타입 빈을 생성되는 것을 확인할 수 있다.
- provider 의 get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
- 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
7. 정리
- 싱글톤과 프로토타입 빈을 함께 사용할 때 발생할 수 있는 문제는 DL을 통해 해결할 수 있다.
- 해결 방식으로는 자바 표준 방식인 JSR-330 Provider와 스프링에서 제공하는ObjectFactory, ObjectProvider방식이 있다.
- 스프링 외에 다른 컨테이너를 사용할 때는 JSR-330 Provider 방식을 사용하자.
- 이외에도 @Lookup 어노테이션을 사용하는 방법도 있다. 기회가 된다면 다음에 정리해보겠다.
'Programming > Spring' 카테고리의 다른 글
[Spring] 스프링 MVC 1편(웹 애플리케이션 이해) (0) | 2024.01.22 |
---|---|
[Spring] 웹 스코프란? (1) | 2024.01.11 |
[Spring] 스프링 컨테이너가 빈을 등록하는 방법 (1) | 2024.01.10 |
[Spring] @Autowired에서 조회 빈이 2개 이상인 경우 (1) | 2024.01.10 |
[Spring] 순환참조 문제 해결 (1) | 2023.10.04 |