김영한님의 [스프링 핵심 원리 - 기본편]을 보고 작성한 글입니다:)


빈 스코프(Bean Scope)란?

빈 스코프는 빈이 스프링 컨테이너에서 존재할 수 있는 범위를 말한다.

여기서 알아야 할 점은 스프링 빈의 생명주기다. 생명주기는 다음과 같다.

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 설정 -> 초기화 콜백 -> 사용 -> 소멸 전 콜백 -> 스프링 종료

 

빈 스코프의 종류

  • 싱글톤
  • 프로토타입
  • 웹 관련 스코프
    • request
    • session
    • application
    • websocket

spring docs(링크)에 기재된 스프링 스코프 설명

 


 

1. 싱글톤 스코프(Singleton Scope)

  • (Default) 스프링 빈은 기본적으로 싱글톤 스코프로 생성
  • 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프 (스프링 컨테이너가 계속 관리)
  • 싱글톤 스코프의 빈을 요청하면 스프링 컨테이너는 본인이 관리하는 스프링 빈 반환
  • 같은 요청이 와도 항상 같은 인스턴스의 스프링 빈을 반환
  • 싱글톤 빈은 스프링 컨테이너가 계속 관리하므로, 빈 초기화 시 초기화 메서드 호출, 빈 소멸 시 종료 메서드가 호출된다.

 

싱글톤은 Default여서 따로 설정을 안하지만, 설정을 하고 싶다면 빈 등록시 @Scope("singleton")을 불이면 된다.

@Scope("singleton")

 


 

2. 프로토타입 스코프(Prototype Scope)

  • 스프링 컨테이너에 요청할 때마다 매번 새로운 빈을 생성해서 반환 (요청이 들어오면 그때 생성함)
  • 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 처리까지만 관여하고 그 이후는 관리 안한다. (요청마다 새로 빈을 생성하는 이유)
  • 그래서 초기화 메서드는 호출되지만 종료 메서드는 호출되지 않는다.
  • 스프링 컨테이너가 클라이언트에 빈을 반환하고나면 클라이언트가 빈의 모든 책임을 가진다.

 

 프로토타입 스코프 설정 - 빈 등록시 다음과 같은 애노테이션을 붙인다.

@Scope("prototype")

 

 

프로토타입 빈와 싱글톤 빈을 함께 사용시 문제점

싱글톤 빈이 프로토타입 빈에 대해 의존성을 갖고 있을 때 문제가 발생한다.

싱글톤 빈은 생성 시점에만 의존관계를 주입받기 때문에, 프로토타입 빈이 싱글톤 빈과 함께 계속 유지된다.

다시 말해 프로토타입 빈을 사용할 때마다 같은 프로토타입의 빈이 사용된다. 

 

하지만 이것은 매번 새로운 객체를 생성하는 프로토타입의 의도와는 맞지 않는다

 

 

이에 대한 해결책은 지정한 빈을 컨테이너에서 대신 찾아주는 DL 기능을 제공하는 ObjectProvider JSR-330 Provider이다.

직접 필요한 의존관계를 조회/탐색하는 것을 Dependency Lookup, 줄여서 DL이라고 부른다.

 

 

ObjectProvider<T>

스프링이 제공하는 DL 서비스. 참고로 과거에는 ObjectFactory가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.

 

사용법

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
     PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
     ...
}
  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
  • prototypeBeanProvider.getObject()를 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • ObjectProvider는 빈으로 등록한 적이 없어도 스프링이 자동으로 만들어서 주입해준다.

 

특징

  • 옵션, 스트림 처리등 편의 기능이 많다.
  • 별도의 라이브러리가 필요 없다.
  • 스프링에 의존적이다.

 

 

JSR-330 Provider<T>

마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.

이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.

 

사용법

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
     PrototypeBean prototypeBean = provider.get();
     ...
}
  • provider의 get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
  • provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • Provider는 빈으로 등록한 적이 없어도 스프링이 자동으로 만들어서 주입해준다.

 

특징

  • 별도의 라이브러리가 필요하다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

 


 

3. 웹 관련 스코프

  • 웹 관련 스코프는 웹 환경에서만 동작
  • 싱글톤 스코프처럼 스프링 컨테이너가 생성 시점부터 종료시점까지 관리해준다.
  • 따라서 초기화 메서드, 종료 메서드 모두 호출된다.

 

웬 관련 스코프 종류

  1. request: HTTP 요청 하나가 들어오고 나갈 떄까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리
  2. session: HTTP Session과 동일한 생명주기를 가지는 스코프
  3. application: Servlet Context와 동일한 생명주기를 가지는 스코프
  4. websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

 

모두 동작 방식은 비슷하므로 request 스코프에 대해서만 살펴보자.

 

requst 스코프

@Component
@Scope("request")
public class MyLogger {
}

로그를 출력하기 위한 MyLogger 클래스를 만들어보자. @Scope(value = "request") 를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.

 

컨트롤러와 서비스를 만들어 로그를 출력하기 위해 MyLogger 를 의존관계 주입받는다.

 

그리고 스프링 애플리케이션을 실행 시키면 오류가 발생한다. 그 이유는?

request 스코프 빈은 실제 HTTP 요청이 들어와야 생성된다. 하지만 애플리케이션 실행 시점에는 아직 요청이 들어오기 전이여서 myLogger가 생성되지 않았다. 그래서 빈을 생성할 수 없기 때문에 오류가 발생한 것이다.

 

 

해결방법 - 프록시 방식

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}

아까의 코드에 proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가해주자.

  • 적용 대상이 인터페이스가 아닌 클래스면, ScopedProxyMode.TARGET_CLASS 를 선택
  • 적용 대상이 인터페이스면, ScopedProxyMode.INTERFACES 를 선택

@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면, 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.

따라서 애플리케이션 생성 시점에 HTTP 요청과 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다. (스프링 컨테이너에 가짜 프록시 객체 등록, 의존관계 주입도 가짜 프록시 객체가 주입된다.) 

 

클라이언트가 myLogger의 로직을 호출하면, 가짜 프록시 객체의 메서드가 호출된다.

그러나 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고있어서 가짜 프록시 객체가 진짜 myLogger.logic() 를 호출한다.

 

 

동작 정리

  • CGLIB이라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입
  • 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • 가짜 프록시 객체는 실제 request scope와 관계 없다. 그냥 가짜 객체로 내부에 단순한 위임 로직만 있고 싱글톤처럼 동작한다.

 

특징 정리

  • 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
  • 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
  • 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
  • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다. (다형성)

 

주의❗

프록시 방식를 통해 마치 싱글톤을 사용하는 것 같지만, 실제로는 다르게 동작하기 때문에 주의해서 사용해야 한다. 

이런 특별한 스코프는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.

+ Recent posts