본문 바로가기
Spring

Bean Scope - 스프링 공식문서가 말하는 빈 스코프

by irerin07 2024. 1. 26.
728x90
들어가기에 앞서, 해당 문서는 스프링 공식 문서를 번역한 포스트 입니다.
해당 문서에서는 XML 기반으로 빈 설정을 하고 있는데 이는 어노테이션 기반으로도, 자바코드로도 설정 할 수 있습니다.

개인적으로 공부 할 요량으로 한 포스팅이다 보니 번역 상태가 굉장히 좋지 않습니다.
가능하시다면 원문을 읽으시는것을 추천 드립니다.
  • 빈 정의를 생성한다는 것은,  클래스의 실제 인스턴스를 만드는 방법을 알려주는 일종의 레시피를 만드는 것과 같다고 할 수 있습니다.
  • 빈 정의를 일종의 레시피로 본다는 것은 우리가 레시피 하나만 있으면 여러 객체의 인스턴스를 만들 수 있다는 점에서 굉장히 중요합니다.
  • 객체 생성에 필요한 의존성이나 설정들을 제어하는 것 이외에도 우린 빈 정의(Bean Definition)를 통해 해당 객체의 스코프(범위) 역시 제어할 수 있습니다.
  • 객체의 스코프를 빈 정의에서 설정할 수 있기 때문에 자바 클래스 레벨에서 이를 설정 할 필요가 없어지게 됩니다.
  •  스프링 프레임워크에서 지원하는 스코프는 총 6개가 있습니다. 
    • 그 중 4개는 ApplicationContext를 사용해야만 사용 가능합니다.
Scope Description
singleton (기본값) 해당 빈이 하나의 Spring IoC Container당  하나의 객체 인스턴스를 생성하도록 설정합니다.
prototype 해당 빈이 여려개의 객체 인스턴스를 생성하도록 설정합니다.
request 해당 빈이 하나의 HTTP Request가 들어오면 객체 인스턴스를 생성하도록 설정합니다. 각각의 HTTP Request는 자신만의 객체 인스턴스를 가지게 됩니다. web-aware 스프링 ApplicationContext가 필요합니다.
session request와 비슷하지만 해당 HTTP Session 마다 객체 인스턴스를 생성합니다. web-aware 스프링 ApplicationContext가 필요합니다.
application request와 비슷하지만 ServletContext마다 객체 인스턴스를 생성합니다. web-aware 스프링 ApplicationContext가 필요합니다.
websocket request와 비슷하지만 WebSocket마다 객체 인스턴스를 생성합니다. web-aware 스프링 ApplicationContext가 필요합니다.

 

thread 스코프도 사용할 수 있지만 기본적으로 등록되어있지 않습니다.
더 자세히 알고 싶으시다면 SimpleThreadScope를 확인해주세요.
SimpleThreadScope, 혹은 커스텀 스코프를 등록하고자 하신다면 커스텀 스코프 사용하기를 참고해주세요.

 

The Singleton Scope

  • 단 하나의 싱글턴 빈 인스턴스만 생성되고 관리 됩니다.
  • 빈 요청이 들어오면 요청된 빈 ID를 확인하여 매칭되는 빈을 스프링 컨테이너가 반환해줍니다.
  • 즉, 어떤 빈의 스코프를 싱글턴으로 설정시, 스프링 IoC 컨테이너는 해당 빈의 인스턴스를 단 하나만 생성합니다.
  • 이런 싱글 인스턴스는 싱글턴 빈 캐시에 저장되고, 후속 요청이나 참조 발생시 캐시된 빈을 반환합니다.

  • 스프링의 싱글턴은 Gang of Four(GoF)의 디자인 패턴에서 말하는 싱글턴과는 다릅니다.
  • GoF에서의 싱글턴은 ClassLoader당 클래스의 객체가 단 하나의 인스턴스만 생성되도록 스코프를 하드코딩하는 방식이라면, 스프링에서는 컨테이너당, Bean당 하나만 생성됩니다.
    • 따라서 만약 하나의 스프링 컨테이너에서 특정 클래스의 빈을 정의했다면, 스프링 컨테이너는 해당 클래스의 인스턴스를 오직 하나만 만듭니다.

The Prototype Scope

  • 싱글턴이 아닌 프로토타입으로 설정된 빈은 해당 빈에 요청이 들어올때마다 새로운 빈 인스턴스를 생성합니다.
  • 컨테이너에서 getBean() 메서드를 통해 매번 새로운 빈을 받아 다른 빈에 주입되거나 반환 됩니다.
  • 싱글턴 빈은 stateless한 빈에 사용하고, 프로토타입 빈은 stateful한 빈에 사용하도록 합니다.

 

  • 위 그림에서 주의할 점은 DAO는 프로토타입으로 설정되는 경우가 거의 없습니다. 위 그림은 이전 예시를 재사용하여 만든 예시입니다.
  • 스프링은 프로토타입 빈의 lifecycle(생명주기)을 관리하지 않습니다.
    • 컨테이너가 인스턴스화 하고, 설정하고, 생성한 다음 클라이언트에게 건내주고 해당 프로토타입 인스턴스에 대해 추가적인 기록(관리)을 하지 않습니다.
  • Initialization life cycle callback 메서드는 스코프 구분 없이 호출 되지만, 프로토타입 빈의 경우 destruction lifecycle callback 은 호출되지 않습니다.
    • 프로토타입 스코프 오브젝트의 뒷처리와 해당 오브젝트가 지니고 있던 자원들의 정리는 클라이언트 코드에서 처리해야 합니다.
    • 만약 스프링 컨테이너가 이런 뒷처리를 하도록 설정하고자 한다면, 커스텀한 bean post-processor를 사용할 수 있습니다.
      • bean post-processor는 정리되어야 할 빈들의 참조를 가지고 있습니다.
  • 프로토타입 빈과 관련된 스프링 컨테이너의 역할은 자바의 new 연산자를 대체하는것에 있습니다. 이 시점을 넘어간(빈의 생성 시점) 이후의 생명주기 관리는 온전히 클라이언트의 몫입니다.
    • 스프링 컨테이너의 빈 생명주기를 더 보고 싶으시면 Lifecycle Callbacks를 참고하세요.

 

Singleton Beans with Prototype-bean Dependencies - 프로토타입 빈에 의존성을 가지는 싱글턴 빈

  • 만약 프로토타입 빈에 의존성을 가지는 싱글턴 빈을 사용하게 된다면, 인스턴스화 시점에 의존성이 해결됩니다.
  • 따라서, 프로토타입 빈을 싱글턴 빈에 의존성 주입하는 경우, 새로운 프로토타입 빈이 인스턴스화 되고 해당 싱글턴 빈에 주입되게 됩니다.
  • 이 프로토타입 인스턴스는 해당 싱글턴 빈에 제공되는 유일한 인스턴스입니다.
  • 싱글턴 빈은 스프링 컨테이너에 의해 인스턴스화 되며 단 한 번 생성되고 의존성 주입도 단 한 번 발생하기 때문에, 런타임에 반복적으로 새로운 프로토타입 빈을 싱글턴 빈에 의존성 주입하는 것은 불가능합니다.
    • 만약 런타임에 새로운 프로토타입 빈의 인스턴스가 필요로 하는 경우에는 Method Injection을 참고하세요

Request, Session, Application, and WebSocket Scopes

  • request, session, application, websocket 스코프는  web-aware 스프링 ApplicationContext 구현체를 사용해야만 사용할 수 있습니다.(XmlWebApplicationContext등)
    • 만약 ClassPathXmlApplicationContext등을 사용하면 IllegalStateException을 발생시키며 알수 없는 빈 스코프는 사용할 수 없다 알려줍니다.

Request scope

@RequestScope
@Component
public class LoginAction {
	// ...
}
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
  • 스프링 컨테이너는 HTTP request가 발생할 때마다 loginAction 빈 정의를 사용해 LoginAction 빈을 생성합니다.
  • 해당 객체의 내부 상태는 얼마든지 변경되어도 동일한 loginAction 빈정의로 생성된 다른 인스턴스에는 영향을 미치지 않는데, 이는 HTTP request가 발생할 때마다 새로운 인스턴스를 만들어내고 내부 상태의 변화는 서로 알 수 없기 때문입니다.
    • 리퀘스트 스코프 빈은 리퀘스트에 특정됩니다.
  • 리퀘스트가 처리 완료 되면 해당 리퀘스트의 빈은 버려집니다.

Session Scope

@SessionScope
@Component
public class UserPreferences {
	// ...
}
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
  • 스프링 컨테이너는 userPreferences 빈 정의를 사용하여 하나의 HTTP session의 수명에 대한 하나의 새로운 UserPreferences 빈 인스턴스를 생성합니다.
  • request 스코프 빈과 비슷하게 이들 역시 내부 상태가 변경 되어도 동일한 빈 정의로 생성된 다른 빈 인스턴스에는 영향을 끼치지 않습니다.
  • 세션이 끝나게 되면, 해당 세션의 빈도 버려집니다.

Application Scope

@ApplicationScope
@Component
public class AppPreferences {
	// ...
}
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
  • 스프링 컨테이너는 appPreferences 빈 정의를 사용해 전체 웹 애플리케이션에 사용할 AppPreferences 빈 객체를 생성합니다.
  • appPreferences 빈은 ServletContext레벨의 범위를 가지고, ServletContext 애트리뷰트에 저장됩니다.
  • 어떻게 보면 싱글턴 빈과 비슷하다 느껴지지만 둘의 주요한 차이점은 다음과 같습니다.
    • 스프링 애플리케이션 스코프 빈은 ServletContext당 하나의 싱글턴 빈을 생성하고 스프링 싱글턴 스코프 빈은 ApplicationContext(웹 애플리케이션에는 하나 이상의 ApplicationContext가 있을 수 있습니다.)당 하나의 싱글턴 빈을 생성합니다.
    • 스프링 애플리케이션 스포크 빈은 ServletContext 애트리뷰트로 표시되고 외부로 노출됩니다.

WebSocket Scope

  • 웹 소켓의 수명과 연관된 스코프입니다. 자세한 내용은 WebSocket scope 를 참조하세요.

Scoped Beans as Dependencies

  • 스프링 IoC 컨테이너는 빈의 인스턴스화 외에도 의존성을 연결하는 일까지 관리합니다.
  • 만약 HTTP request 스코프의 빈을 request 스코프 빈의 생명주기보다 긴 빈에 주입하고자 하는 경우 우리는 AOP 프록시를 대신 주입해줄 수 있습니다.
    • 실제 스코프 오브젝트와 동일한 공용 인터페이스를 노출하는 프록시 객체를 주입해주는 방식입니다.
      • 이 프록시 객체는 관련 스코프에 있는(예를 들어 HTTP request) 실제 타겟 객체를 검색하고 메서드 호출을 실제 객체에 위임할 수 있습니다.
싱글톤 스코프로 설정된 두 빈들에는 <aop:scoped-proxy/>를 사용할 수 있습니다. 이 참조는 직렬화 가능한 중간 프록시를 거치므로 역직렬화시에 타겟 싱글턴 빈을 다시 가져올 수 있습니다.

프로토타입 스코프의 빈에 <aop:scoped-proxy/>를 선언하게 되면 공유 프록시에서의 모든 메서드 호출이 새로운 인스턴스를 생성하게 하고, 새로 생성된 인스턴스에 호출을 전달합니다.

scoped proxies(범위 지정된 프록시)는 생명주기가 짧은 스코프의 빈에 lifecycle-safe한 방식으로 접근할 수 있는 유일한 방법이 아닙니다.
주입포인트(생성자 인수, 수정자 인수 혹은 오토와이어된 필드)를 ObjectFactory<MyTargetBean>으로 선언하여 필요할때마다 getObject() 호출을 통해 인스턴스를 계속 유지하거나 따로 저장할 필요 없이 현재 인스턴스를 얻어올 수 있도록 할 수 있습니다. 

추가적으로 ObjectProvider<MyTargetBean>을 선언하면 getIfAvailable이나 getIfUnique같은 추가적인 변형메서드를 사용할 수 있습니다.

JSR-330 변형 버전은 Provider라고 불리며 Provider<MyTargetBean>으로 선언하고 get() 호출을 사용해 검색을 시도합니다. JSR-330 관련 정보를 이곳을 참고해주세요.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop
		https://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- HTTP session 스코프의 빈 설정. 프록시 객체를 노출하도록 설정 -->
	<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
		<!-- instructs the container to proxy the surrounding bean -->
		<aop:scoped-proxy/> ⑴
	</bean>

	<!-- 위에서 설정된 빈의 프록시가 주입되는 싱글턴 스코프 빈 -->
	<bean id="userService" class="com.something.SimpleUserService">
		<!-- 프록시 userPreferences 빈의 참조 정보 -->
		<property name="userPreferences" ref="userPreferences"/>
	</bean>
</beans>
  • ⑴ 로 표시된 라인이 Proxy를 정의하는 부분입니다.
  • 이런 프록시 객체를 생성하기 위해서는 프록시 객체를 생성할 빈 정의에 <aop:scoped-proxy/>를 사용해 줍니다.
  • 다음 싱글턴 빈 정의 예시를 보고, 앞서 말한 스코프들(request, session 혹은 custom-scope level)을 정의하기  위해 무엇이 필요한지 비교해봅시다. 
    • 다음 예시에서 userPreferences 빈 정의는 아직 완료되지 않은 상태입니다.
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

<bean id="userManager" class="com.something.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>
  • 위 예시에서 userManager 싱글턴 빈은 HTTP session 스코프의 userPreference 빈을 참조하도록 되어 있습니다.
  • 여기서 주목할 부분은 userManager 빈은 싱글턴이라는 점인데, 싱글턴 빈은 컨테이너당 단 한 번 인스턴스화 되고 이 때 의존성 주입 역시 단 한 번 발생하게 됩니다.
  • 즉, userManager 빈은 자신이 인스턴스화 된 시점에 주입 받은 userPreference 객체만으로 동작 하게 된다는 뜻입니다.
  • 이런 동작 방식은 자신보다 스코프(생명주기)가 짧은 빈을 주입 받았을때에 원하는 방식이 아닙니다.
    • 이보다는, 싱글턴 userManager 객체와, userManager가 특정 HTTP session마다의 userPreferences 객체를 사용하는 동작 방식이 필요합니다.
  • 따라서, 컨테이너는 scoping mechanism(HTTP request, session등)에서 실제 UserPreferences 객체를 가져올 수 있도록 UserPreferences 클래스(이상적으로는 UserPreferences 인스턴스의 객체)와 정확히 동일한 공용 인터페이스를 노출하는 객체를 생성합니다.
  • 그리고 컨테이너는 이 프록시 객체를 userManager빈에 주입하는데, userManager빈은 해당 객체가 프록시인지 알지 못합니다.
  • 위 예시에서 UserManager인스턴스가 의존성 주입된 UserPreferences 객체의 메소드를 호출하게 되면, 실제로는 프록시 객체에 메서드를 호출하게 됩니다.
  • 메서드가 호출 되면 프록시는 HTTP session에서 실제 UserPreferences 객체를 가지고 와 해당 UserPreferences 객체에 메서드 호출을 위임하게 됩니다.
  • 그렇기에 다음과 같이 설정할 필요가 있습니다.
<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
	<aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>

Choosing the Type of Proxy to Create

  • 기본적으로 스프링 컨테이너가 <aop:scoped-proxy/>설정된 빈의 프록시를 생성할 때, CGLIB 기반의 클래스 프록시가 생성됩니다.
CGLIB 프록시들은 private 메서드들을 인터셉트 하지 않습니다. CGLIB 프록시에서 private 메서드를 호출해도 실제 타겟 오브젝트에 메서드 호출을 위임하지 않습니다.
  • CGLIB 방식을 원하지 않고 스프링 컨테이너에게 기본 JDK 인터페이스 기반 프록시를 생성하게 하고자 한다면 <aop:scoped-proxy/>에서 proxy-target-class 를 false 값으로 주어 설정할 수 있습니다.
  • JDK 기반 프록시를 사용하면 해당 빈은 반드시 하나 이상의 인터페이스를 구현해야 하고,  해당 빈을 주입받는 다른 빈들은 인터페이스들 중 하나를 통해 빈 참조를 해야 합니다.
<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
	<aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
	<property name="userPreferences" ref="userPreferences"/>
</bean>
  • class-based proxying과 interface-based proxying 관련 정보는 Proxying Mechanisms 를 참고하세요.

Injecting Request/Session References Directly

  • factory scope(BeanFactory혹은 Application context를 통해 만들어진 빈들의 scope)의 대안으로 스프링의 WebApplicationContext는 HttpServletRequest, HttpServletResponse, HttpSession, WebRequest, 그리고 JSF가 있는 경우 FacesContext와 ExternalContext를 타입 기반 오토와이어링을 통해 스프링에서 관리하는 빈들에 주입할 수 있습니다.
  • 이경우 스프링은 해당 객체의 프록시를 주입해주게 됩니다.

Custom Scopes

  • 빈 스코프 메커니즘은 확장이 가능합니다. 직접 스코프를 정의하거나, 이미 존재하는 스코프를 재정의 할 수 있습니다.
    • 주의할 점은 이미 존재하는 스코프를 재정의 하는 것은 bad practice이며 내장된 singleton과 prototype 스코프는 재정의 할 수 없습니다.

Creating a Custom Scope

  •  커스텀 스코프를 스프링 컨테이너에 통합하기 위해서는 org.springframework.beans.factory.config.Scope 인터페이스를 구현해야 합니다.
  • 커스텀 스코프를 만들기 위해 아이디어 혹은 예시를 찾고 싶다면, 스프링 프레임워크에서 제공하는 Scope 구현체들 혹은 Scope javadoc을 참고하세요.
  • Scope 인터페이스는 어떤 스코프에서 객체를 가져오거나, 스포크에서 제거 혹은 파괴되도록 하는 4가지의 메서드를 가지고 있습니다.
  • 예를 들어 session 스코프 구현체는 session 스코프 빈을 반환하는데, 만약 존재하지 않는 경우 메서드는 추후 참조를 위해 세션에  해당 빈의 새로운 인스턴스를 바운드 시킨 후 해당 인스턴스를 반환합니다.
Object get(String name, ObjectFactory<?> objectFactory)
  • 또 다른 예시로 다음 메서드는 밑바탕되는 스코프에서 객체를 제거합니다. 제거 된 객체를 반환할 수도 있지만 지정된 이름의 객체가 없는 경우 null을 반환하도록 할 수 있습니다.
Object remove(String name)
  • 다음 메서드는 스코프가 파괴(종료)되거나 스코프 안에 있는 특정 객체가 파괴되는 경우 실행되는 callback을 등록합니다.
void registerDestructionCallback(String name, Runnable destructionCallback)
  • destruction callbacks 관련 자세한 내용은 javadoc 혹은 Spring scope implementation을 참고하세요.
  • 다음 메서드는 밑바탕되는 스코프의 conversation identifier는 반환합니다.
    • conversation이란 둘 이상의 participants들 사이에서 발생하는 지속적인 대화형 통신을 뜻합니다. requests, messages 혹은 이벤트등의 교환으로 통신을 하게 됩니다.- ChatGPT
String getConversationId()
  • 이 identifier는 각 스코프마다 다릅니다. session 스코프 구현체라면, 이 identifier는 session identifier가 될 수 있습니다.

Using a Custom Scope

  • 하나 이상의 커스텀 Scope 구현체를 만들고 테스트 했다면, 스프링 컨테이너에게 해당 스코프들을 알려줘야 합니다.
  • 다음 메서드는 스프링 컨테이너로 새로운 Scope 구현체를 등록하는 메서드입니다.
void registerScope(String scopeName, Scope scope);

 

  • 이 메서드는 ConfigurableBeanFactory에 선언되어 있으며, 스프링을 통해 제공되는 대부분의 ApplicationContext 구현체의 BeanFactory property를 통해 사용할 수 있습니다.
  • registerScope의 첫번째 인수는 스코프의 유니크한 이름을 넣습니다. 
  • 두 번째 인수는 등록해서 사용하고자 하는 커스텀 스코프의 실제 인스턴스를 넣습니다.
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
  • 그 다음, 생성한 커스텀 스코프의 규칙을 준수하는 빈 정의를 작성합니다.
<bean id="..." class="..." scope="thread">
  • 사용자 정의 Scope 구현을 통해 Scope를 프로그래밍적으로 등록하는 것에 제한되지 않고, CustomScopeConfigurer 클래스를 사용하여 Scope 등록을 선언적으로 수행할 수도 있습니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop
		https://www.springframework.org/schema/aop/spring-aop.xsd">

	<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
		<property name="scopes">
			<map>
				<entry key="thread">
					<bean class="org.springframework.context.support.SimpleThreadScope"/>
				</entry>
			</map>
		</property>
	</bean>

	<bean id="thing2" class="x.y.Thing2" scope="thread">
		<property name="name" value="Rick"/>
		<aop:scoped-proxy/>
	</bean>

	<bean id="thing1" class="x.y.Thing1">
		<property name="thing2" ref="thing2"/>
	</bean>

</beans>
<aop:scoped-proxy/>를 <bean> 으로 감싸는 경우, 팩토리 빈 자체가 스코프를 갖게 되며, getObject()에서 반환되는 객체는 스코프가 적용되지 않습니다.
728x90