1. Bean Scope

//컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class TestBean {}

//수동 등록
@Scope("prototype")
@Bean
PrototypeBean TestBean() {
	return new TestBean();
}

- 빈이 존재할 수 있는 범위를 의미한다.

- 스프링 빈은 기본적으로 싱글톤 스코프로 생성되기 때문에, 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다.

 

2. Scope 종류

종류 설명
singleton - 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- 하나의 bean 정의에 대해서 컨테이너 내에 단 하나의 객체만 존재한다.
prototype - 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입, 그리고 초기화까지만 관여하고 더는 관여하지 않는 비교적 짧은 범위의 스코프이다.
- 모든 요청에서 새로운 객체를 생성한다.
- 프로토타입 빈을 받은 클라이언트가 객체를 관리해야 한다. 종료 메서드가 따로 호출되지 않기 때문에, 클라이언트가 직접 종료 메서드를 호출 해야한다.
- 하나의 bean 정의에 대해서 다수의 객체가 존재할 수 있다.
request - 웹 요청이 들어오고 나갈 때까지 유지되는 스코프이다.
- 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다. 즉, 각각의 HTTP request는 자신만의 객체를 가진다.
session - 웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
- 하나의 HTTP Session의 생명주기 안에 단 하나의 객체만 존재한다.
global session - 하나의 global HTTP Session의 생명주기 안에 단 하나의 객체만 존재한다. 일반적으로 portlet context 안에서 유효하다.
application - 웹의 ServletContext와 동일한 생명주기를 가지는 스코프이다.
websocket - 웹 소켓과 동일한 생명주기를 가지는 스코프이다.

 

3. Singleton vs. Non-Singleton

구분 종류
싱글톤으로 적합한 객체 ① 상태가 없는 공유 객체: 상태를 가지고 있지 않은 객체는 동기화 비용이 없기 때문에 매번 해당 객체를 참조하는 곳에서 새로운 객체를 생성할 이유가 없다.
② 읽기용으로만 상태를 가진 공유 객체: 상태를 가지고 있지만 읽기 전용이므로 여전히 동기화 비용이 들지 않는다. 따라서 매 요청마다 새로운 객체를 생성할 필요가 없다.
③ 공유가 필요한 상태를 지닌 공유 객체: 객체 간 반드시 공유해야 할 상태를 지닌 객체가 하나 있는 경우, 쓰기 접근에 대한 고려가 있다면 싱글톤이 적합할 수도 있다.
④ 쓰기가 가능한 상태를 지니면서도 사용 빈도가 매우 높은 객체: 애플리케이션 안에서 사용 빈도가 매우 높다면, 쓰기 접근에 대한 동기화 비용을 감안하고서라도 싱글톤을 고려할 수 있다. 특히 장시간에 걸쳐 매우 많은 객체가 생성되고, 해당 객체가 매우 적은 양의 쓰기 상태를 가지고 있을때, 그리고 객체 생성 비용이 매우 클 때에 가장 적합하다고 볼 수 있다.
비싱글톤으로 적합한 객체 ① 쓰기가 가능한 상태를 지닌 객체: 쓰기가 가능한 상태가 많아서 동기화 비용이 객체 생성 비용보다 크다면 싱글톤으로 적합하지 않다.
② 상태가 노출되지 않은 객체: 내부 상태를 외부에 노출하지 않는 빈을 참조하여 다른 의존 객체와는 독립적으로 작업을 수행하는 의존 객체가 있다면, 싱글톤보다 비싱글톤 객체를 사용하는 것이 나을 수도 있다.

 

4. 싱글톤 빈과 프로토타입 빈을 함께 사용하는 방법

- 스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그렇기 때문에 프로토타입 빈이 새로 생성되기는 하지만, 생성 시점에만 의존관계 주입을 받는 싱글톤 빈과 함께 계속 유지되는 문제가 발생할 수 있다.

- 문제 해결 방법

종류 설명
스프링 컨테이너에 요청 - 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새롭게 요청한다.
- 의존관계를 외부에서 주입(DI) 받는게 아니라, 직접 필요한 의존관계를 찾는다(DL).
- 그러나 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되기 때문에 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
ObjectFactory, ObjectProvider - ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는 DL 기능을 제공한다.
- 과거에는 ObjectFactory가 위의 기능을 제공했는데, 여기에 편의 기능이 추가된 것이 ObjectProvider이다.
- ObjectFactory: 기능이 단순하고 별도의 라이브러리가 필요 없다. 스프링에 의존한다.
- ObjectProvider: ObjectFactory를 상속 받는다. 옵션, 스트림 처리 등 편의 기능이 많고 별도의 라이브러리가 필요 없다. 스프링에 의존한다.
- getObject() 메서드를 통해서 항상 새로운 프로토타입 빈이 생성된다. 해당 메서드가 호출되면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL).
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하기 때문에 단위테스트를 만들거나 mock 코드를 만들기 훨씬 쉽다.
- 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 하는 경우만 아니면, 프로토타입 빈 생성에 가장 많이 사용한다.
JSR-330 Provider - javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법이다. 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
- get() 메서드를 통해서 항상 새로운 프로토타입 빈이 생성된다. 해당 메서드가 호출되면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다(DL).
- 자바 표준이고, get() 메서드 하나로 기능이 매우 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉽다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
//ObjectProvider 예시
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
	PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}

//JSR-330 Provider 예시: Provider 인터페이스
public interface Provdier<T> {
	T get();
}

//JSR-330 Provider 예시: 실행 코드
@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
	PrototypeBean prototypeBean = provider.get();
	prototypeBean.addCount();
	int count = prototypeBean.getCount();
	return count;
}

 

4.1 DL(Dependency Lookup)

- 의존관계를 외부에서 주입(DI) 받는게 아니라, 직접 필요한 의존관계를 찾는 것을 의미한다.

- ObjectProvider, JSR330-Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

 

5. 스프링 애플리케이션을 실행하는 시점에서 웹 스코프 빈을 생성하는 방법

- 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, 웹 스코프 빈은 실제 고객의 요청이 와야 생성되기 때문에, 생성이 되지 않는다.

- 문제 해결 방법

종류 설명
ObjectProvider - 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다.
- ObjectProvider.getObject() 메서드를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- ObjectProvider.getObject() 메서드를 호출하는 시점에는 HTTP 요청이 진행 중이므로 request scope 빈의 생성이 정상 처리된다.
Proxy - 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다.
- proxyMode = ScopedProxyMode.TARGET_CLASS를 추가해서 설정할 수 있다.
- 적용 대상이 클래스인 경우 TARGET_CLASS를, 인터페이스인 경우 INTERFACE를 선택하면 된다.
- 해당 설정을 하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서 진짜 클래스를 상속 받은 가짜 프록시 클래스를 만들고, HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입할 수 있다.
- 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
- 반드시 웹 스코프가 아니어도 proxy를 사용할 수 있다.
//Proxy 예시
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TestBean {}

 

참고

- 『스프링 핵심 원리 - 기본편』

- https://gmlwjd9405.github.io/2018/11/10/spring-beans.html

'Spring' 카테고리의 다른 글

Logging  (0) 2021.11.11
DispatcherServlet  (0) 2021.11.08
Bean LifeCycle & Callback  (0) 2021.10.11
의존관계 주입 시 bean이 2개 이상일 때 해결 방법  (0) 2021.10.11
Lombok library가 제공하는 애노테이션  (0) 2021.10.11