스프링 핵심 원리 정리 - 9
11 Oct 2020빈 스코프
스코프는 빈이 존재할 수 있는 범위를 뜻하며 스프링 빈은 기본적으로 싱글톤 스코프로 생성되어 스프링 컨테이너가 시작될 때 생성되어 컨테이너가 종료될 때까지 유지된다. 스프링이 지원하는 스코프는 다음과 같다.
- 싱글톤(Singleton): 기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입(Prototype): 스프링 컨테이너는 프로타타입 빈의 생성과 의존관계 주입까지만 관여하는 매우 짧은 범위의 스코프
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈 때까지 유지
- session: 웹 세션이 생성되고 종료될 때까지 유지
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지
빈 스코프를 지정할 때는 컨포넌트 스캔으로 자동 등록할때나 수동으로 등록할 때 모두 @Scope()
어노테이션을 붙여주면 된다.
// 컴포넌트 스캔
@Scope("prototype")
@Component
public class SampleBean1 {}
// 수동 등록
@Scope("request")
@Bean
SampleBean2 HelloBean () {
return new HelloBean();
}
프로토타입 스코프
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환하지만, 프로토타입 스코프로 조회할 경우 빈을 생성하고 필요한 의존관계를 주입하고 빈을 반환한다. 그 후 관리는 하지 않으며 요청이 올 때마다 새로운 빈을 생성하고 반환하게 된다. 프로토 타입 빈을 관리할 책임은 클라이언트에게 있다. 클라이언트가 @PreDestroy
같은 종료 메서드를 호출하지 않는 한, 프로토 타입의 종료 메서드는 호출되지 않는다.
싱글톤 빈과 함께 사용할 때 문제점
프로토타입 스코프의 빈은 요청할 때마다 새로운 객체 인스턴스를 생성해서 반환하지만, 싱글톤 빈과 함께 사용할 때는 의도대로 되지 않는다. 싱글톤 빈 내부의 프로토타입 빈은 처음 요청이 들어오면 생성하고 주입이 끝나고, 그 후 요청이 들어오면 이미 만들어진 프로토 타입 빈이 사용되기 때문에 프로토타입처럼 사용할 수 없다.
ObjectFactory, ObjectProvider
필요한 의존관계를 찾는 것을 DL(Dependency Lookup) 의존관계 조회라고 한다. 스프링에서 DL 기능을 제공하는 것이 ObjectFactory
와 이를 상속한 ObjectProvider
이다. 프로바이더가 제공하는 getObject()
를 통해 항상 새로운 프로토타입 빈을 생성한다.
- ObjectFactory: 기능이 단순하며 별도의 라이브러리가 필요없으나 스프링에 의존적
- ObjectProvider: ObjectFactory를 상속하며 옵션, 스트림 처리 등 편의 기능이 많고 스프링에 의존적
import org.springframework.beans.factory.ObjectProvider;
@Scope("prototype")
public class PrototypeBean {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return count;
}
}
@Scope("singleton")
public class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
JSR-330 Provider
스프링 기능을 사용하지 않고 javax.inject.Provider
라는 JSR-330 자바 표준을 사용하는 방법도 있다. 이 방법을 사용하기 위해서는 gradle에 라이브러리를 추가해야 한다. 스프링의 ObjectProvider와는 다르게 get()
메서드를 사용한다.
// build.gradle
dependencies {
implementation 'javax.inject:javax.inject:1'
}
import javax.inject.Provider;
@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;
}
}
웹 스코프
웹 스코프는 웹 환경에서만 동작하며 스프링이 해당 스코프의 종료시점까지 관리한다. 웹 환경이 동작하려면 라이브러리를 추가해야 한다.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
@Scope("request")
로 설정한 후 앱을 실행하면 예상과 다르게 오류가 발생하게 된다. 리퀘스트 스코프는 말 그대로 실제 클라이언트의 요청이 있어야 생성되기 때문이다.
Error creating bean with name 'xxxx': Scope 'request' is not active for the
current thread; consider defining a scoped proxy for this bean if you intend to
refer to it from a singleton;
ObjectProvider
이 문제를 해결하기 위해서 먼저 ObjectProvider를 사용할 수 있다.
- ObjectProvider.getObject()를 호출하는 시점까지 리퀘스트 스코프 빈의 생성 지연
- getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 리퀘스트 스코프 빈 생성 정상 처리
Proxy
또 다른 방법으로는 @Scope에 proxyMode
옵션을 추가하는 것이다. 이렇게하면 가짜 프록시 클래스를 만들어 리퀘스트와 상관없이 다른 빈에 미리 주입하여 사용할 수 있다.
- 적용 대상이 클래스면
ScopedProxyMode.TARGET_CLASS
, 적용 대상이 인터페이스면ScopedProxyMode.INTERFACE
- CGLIB 라이브러리가 바이트코드 조작하여 프록시 객체 생성하여 의존관계 주입
- 요청이 오면 프록시 객체가 내부에 진짜 빈을 요청
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)