본문 바로가기

SpringFramework Core - I. IoC 컨테이너/5. Bean Scopes

5.4.5. 의존성으로서의 Scoped Beans

원문: https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-scopes-other-injection

 

 

스프링 IoC 컨테이너는 객체의 인스턴스화 뿐만 아니라 협력자들을 엮어주는 일도 한다. 예를 들어, 만약 HTTP 요청 scope의 bean을 더 긴 scope를 가진 다른 bean에 주입하는 경우라면, scoped bean에(HTTP 요청 scope) AOP 프록시를 주입해야할 지 모른다. 즉, scoped 객체(HTTP 요청 scope)와 똑같은 public 인터페이스를 표현하는 프록시 객체를 주입해야한다는 것이다. 하지만 이는 실제 대상 객체를 관련 scope(HTTP 요청 scope)로부터 얻어오게 하면서 실제 객체로는 위임 메소드가 호출을 하게 한다. 

 

 

<aop:scoped-proxy/>도 싱글턴 scope bean 사이에서 사용할 수 있다. 그러면 직렬화가 가능한 중간 프록시로 참조가 연결된다. 그 다음에는 직렬화를 풀어서 다시 대상 싱글턴 bean을 얻을 수 있다.

 

프로토타입 bean에 대해 <aop:scoped-proxy/>를 선언할 때는, 공유된 프록시로 메소드를 호출할 때마다 새로운 대상 인스턴스를 생성한 후에 호출을 포워딩시킨다.

 

scope가 더 길거나 같은 bean으로 접근하면서 라이프사이클이 안전하도록 하기 위한 유일한 방법이 scoped 프록시인 것은 아니다. 'ObjectFactory<MyTargetBean>'을 통해 주입 포인트(생성자, setter 매개변수, autowired 필드)를 선언할 수도 있다. 그러면 필요할때마다 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 세션 scope bean -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- 컨테이너에게 해당 bean을 프록시라고 알려줌 -->
        <aop:scoped-proxy/>
    </bean>
    
    
    <!-- 싱글턴 scope bean이 프록시와 함께 위쪽의 bean을 주입함 -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- 프록시화 된 userPreferences bean에 대한 참조 -->
        <property name="userPreferences" ref="userPreferences" />
    </bean>
    
</bean>

프록시를 만들기 위해서는, <aop:scoped-proxy/> 요소를 scoped bean 정의 안에다 집어넣으면 된다. ('생성을 위한 프록시 종류 고르기'와 'XML 스키마 기반의 설정'을 참고하라). 왜 요청, 세션, 커스텀 scope 레벨에서는 <aop:scoped-proxy/> 요소를 설정해야만 할까? 다음의 싱글턴 bean 정의가 그 위쪽에 선언된 scope에 대해 어떤 설정을 해줘야할 지 생각해보고, 그 반대로도 생각해보자. (다음 userPreferences bean 정의가 불완전하다는 점을 주의하라)

<bean id="userPreferences" class="com.something.UserPreferences" scope="session" />

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

싱글턴 bean인 userManager는 HTTP 세션 bean인 userPreferences을 참조로서 주입 받는다. 여기서 중요한 부분은 userManager가 싱글턴 bean이라는 점이다. 이것은 컨테이너에서 정확히 한번만 인스턴스화된다. 그리고 그것의 의존성 역시 한번만 주입된다. 이는 userPreferences가 원래 주입받은 것과 정확히 똑같을 때만 userManager bean이 작동한다는 것을 의미한다. 

 

scope가  짧은 bean을 더 긴 쪽에 주입할 때는 이런 상황을 원하지 않았을 것이다. 아마도 싱글턴 userManager 객체를 원했을 것이고, HTTP 세션의 라이프타임동안 userPreferences를 HTTP 세션 scope로서 가지고 싶었을 것이다. 그래서 컨테이너는 UserPrefereces 클래스와 동일한 public 인터페이스를 표현하는 객체를 만들 것이다. 그리고 그 객체가 scoping 메커니즘(HTTP 요청, 세션 등등)으로부터 실제 UserPreferences 객체를 호출할 수 있게 해줄 것이다. 컨테이너는 이 프록시 객체를 userManager bean에 주입한다. 이 UserPreferences 참조가 프록시라는 사실을 알려주지 않은 채로 말이다. 이 예시에서는 UserManager 인스턴스가 주입된 UserPreferences 객체의 메소드를 호출하면 실제로는 프록시의 메소드를 호출하게 된다. 그러면 프록시는 실제 UserPreferences 객체의 메소드를 HTTP 세션으로부터 불러낸 후, 실제 UserPreferences 객체에게 메소드 호출을 위임한다.

 

그래서 요청과 세션 scope bean들을 주입할 때는 다음 설정이 필요하다.

<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>