스프링 핵심 원리 정리 - 9

빈 스코프

스코프는 빈이 존재할 수 있는 범위를 뜻하며 스프링 빈은 기본적으로 싱글톤 스코프로 생성되어 스프링 컨테이너가 시작될 때 생성되어 컨테이너가 종료될 때까지 유지된다. 스프링이 지원하는 스코프는 다음과 같다.

  • 싱글톤(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)