Bean과 Container의 확장

Spring Framework의 Container는 기본적으로 확장이 되도록 설계되어 있다. 모든 어플리케이션 개발자들이 확장하여 사용할 필요는 없고 확장할 필요성이 있는 경우에 확장하여 사용하도록 한다. 다음 각각의 항목 별로 기본적으로 제공되는 내용과 확장하여 사용할 수 있는 내용을 설명한다.

Bean Scope

Spring Framework에서 지원하는 5가지 Scope에 따라 Bean의 인스턴스 생성 메커니즘이 결정된다. 서비스 Scope은 설계, 개발 단계에서 결정하기 어려우므로, 기본적으로는 Default Scope인 Singleton으로 개발하고, 추후 해당 서비스의 성격에 따라 Scope을 정의하는 것이 좋다.
  • <bean>의 scope 속성값
  • 속성
    설명
    singleton [기본 설정] Spring IoC Container 내에서 Bean 정의 당 하나의 Bean 객체 생성
    prototype 매번 같은 Type의 새로운 Bean 객체 생성
    request WebApplicationContext 유형의 Container 사용 시, Http request 당 하나의 Bean 객체 생성
    session WebApplicationContext 유형의 Container 사용 시, Http session 당 하나의 Bean 객체 생성
    globalSession WebApplicationContext 유형의 Container 사용 시, portlet context 내에서만 유효하며 global Http session 당 하나의 Bean 객체 생성
    이 외에도, custom scope을 통해 신규 Scope에 대해 정의할 수 있다.

Singleton


Singleton Scope은 기본 Scope으로 여러 개의 요청에 대해 하나의 Bean 인스턴스를 생성하여 제공한다. 따라서 Client Request마다 유지해야 하는 Data가 있다면, Singleton Scope의 서비스는 적합하지 않다. 다음은 Singleton Scope의 속성 정의 예시이다.
<bean id="userService" class="com.sds.emp.user.services.impl.UserServiceImpl" scope="singleton”>
     <property name="userDAO" ref="userDAO" />
</bean>
<bean id="userDAO" class="com.sds.emp.user.services.impl.UserDAO”>
중략…
</bean>
위와 같이 singleton scope을 정의 할 수 있지만 scope의 기본 설정값이 singleton이므로 따로 정의해야 할 필요가 없다.

Prototype


Prototype Scope은 요청시마다 Bean 인스턴스를 생성하여 제공한다. 따라서 여러 Client가 동시에 한 Bean 인스턴스에 접근할 수 없다. 다음은 Prototype Scope의 속성 정의 예시이다.
<bean id="userService" class="com.sds.emp.user.services.impl.UserServiceImpl" scope="prototype”>
     <property name="userDAO" ref="userDAO" />
</bean>

※ 일반적으로 인스턴스의 Singleton 여부를 판단하기 위해서 전역변수의 존재 여부를 이용한다. 즉, 전역변수가 존재하지 않은 인스턴스의 경우에는 Singleton, 전역변수가 존재하는 경우에는 Prototype 으로 정의할 수 있다. 그러나 해당 전역변수가 read-only인지 writable 가능한지에 따라서 이 같은 구분은 변경될 수 있다. 따라서 인스턴스를 Singleton으로 생성할지 Prototype으로 생성할지에 대한 여부에 대해서는 개발자들이 해당 Scope의 인스턴스가 메모리에서 어떻게 사용되는지를 이해하는 것이 가장 좋다.
  • Singleton

  • - Shared objects with no state
    - Shared object with read-only state
    - Shared object with shared state : 이 경우에는 Synchronization을 적절하게 사용하여 동시성을 제어하도록 해야 한다.
    - High throughput objects with writable state : 일반적으로 Object Pooling과 같은 기능을 사용하는 것을 예로 들 수 있다. 인스턴스를 생성하는데 많은 비용이 발생하거나 무수히 많은 인스턴스를 관리할 필요가 있는 경우에는 Object Pooling을 사용하고 Pooling 대상이 되는 인스턴스는 Singleton으로 사용할 수 있다. 이 경우에도 Writable State에 변경이 발생할 때 Synchronization을 적절하게 사용해야 한다.
  • Prototype

  • - Objects with writable state
    - Objects with private state

Other Scopes

request, session, globalSession Scope 사용 시 주의 사항은 다음과 같다.
  • Web 기반의 ApplicationContext 사용시에만 이 Scope들을 사용할 수 있으며 그 외의 경우 사용하게 되면 IllegalStateException이 발생한다.
  • Scope이 다른 Bean에서 참조하는 경우 Bean 정의 시 <aop:scoped-proxy/>와 함께 작성해야 한다.(아래의 예시 참고)
    userPreferences Bean은 scope이 session이지만 userService Bean의 scope이 singleton(default가 singleton)이기 때문에 문제가 발생한다. 즉, 매 세션마다 UserPreferences 객체를 만들어줘야 하지만 userService Bean에 의해 UserPreferences 객체가 한 번만 생성되기 때문에 원하던 대로 동작하지 못하는 것이다. 따라서 매 세션 마다 새로운 객체를 만들어서 줄 Proxy를 만들기 위해서 <aop:scoped-proxy/>를 사용하도록 한다.
 <!-- a HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
	<!-- this next element effects the proxying of the surrounding bean -->
     <aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.foo.SimpleUserService">
	<!-- a reference to the proxied 'userPreferences' bean -->
    <property name="userPreferences" ref="userPreferences"/>
</bean>

Custom

신규 Scope을 정의하기 위한 클래스를 생성하고, org.springframework.beans.factory.config.Scope 인터페이스를 implements한다. 또한 CustomScopeConfigurer를 이용하여 신규 정의한 Custom Scope을 등록하여 Custom Scope를 사용할 수 있도록 한다.

해당 프로젝트에 적합한 Scope을 아래의 예시와 같이 직접 정의할 수 있다.
<!-- 신규 Scope 정의를 위한 클래스를 정의하고, 
org.springframework.beans.factory.config.Scope 인터페이스를 implement한다.-->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
	<!-- CustomScopeConfigurer를 이용하여 Custom Scope 등록  -->
    <property name="scopes">
        <map>
            <entry key="thread">
                <bean class="com.foo.ThreadScope"/>
            </entry>
        </map>
    </property>
</bean>
<!-- Custom Scope 사용 -->
<bean id="bar" class="x.y.Bar" scope="thread">
    <property name="name" value="Rick"/>
    <aop:scoped-proxy/>
</bean>

Bean Life Cycle

Bean의 Life Cycle은 다음 그림에서와 같이 Initialization, Activation, Destruction으로 구성된다.

Initialization

Spring Container는 아래 그림에서 보여지는 여러 과정을 통해 구동된다. Spring Bean 클래스가 아래 그림에서 보여지는 각각의 인터페이스들을 구현하였을 때 각각의 메소드들이 호출된다.

Spring Framework에서 지원하는 Life Cycle 메소드를 그대로 사용할 경우 특정한 인터페이스를 구현해야 하므로, 해당 코드가 Spring Framework에 의존적일 수 있게 된다.
즉, 위 그림에서 제시하고 있는 Life Cycle 메소드를 사용하기 위해서는 Spring Bean 클래스에서 해당 Life Cycle 인터페이스 클래스를 구현해줘야 한다. 예를 들어, ApplicationContextAware 인터페이스 클래스를 구현한 Spring Bean에서는 setApplicationContext(ApplicationContext context) 메소드를 작성하고, Spring Bean 내부에서 ApplicationContext를 이용하여 ApplicationContext에서 제공하는 메소드를 호출할 수 있다.
public class IoCServiceImpl1 implements IoCService1, ApplicationContextAware {
    public void setApplicationContext (ApplicationContext context){
        IoCService2 iocService2 = (IoCService2)context.getBean("IoCService2");
    }
}
또다른 예로 MessageSourceAware 인터페이스 클래스의 경우, Spring Container에 정의된 MessageSource를 얻기 위해 사용될수 있다. MessageSourceAware 인터페이스 클래스를 구현한 Spring Bean에서 setMessages(MessageSource messages) 메소드를 작성하여 MessageSource에 접근할 수 있다.
public class IoCServiceImpl1 implements IoCService1, MessageSourceAware {
    private MessageSource messageSource;
	public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }
}
이와는 달리 Bean 속성(init-method, destroy-method) 정의를 통해 특정 인터페이스에 대한 구현없이 별도 Life Cycle 메소드를 정의할 수도 있다. 다음은 init-method 속성이 정의된 applicationContext-user-bean.xml 의 일부이다.
<bean id="com.sds.emp.user.services.UserServiceWithIoCExtended"
	class="com.sds.emp.user.services.impl.UserServiceImplWithIoCExtended" 
	init-method="userInitialize" destroy-method="userDestroy" parent="parent">
</bean>
모든 Bean에 대한 초기화 method 설정은 <beans>의 default-init-method 속성을 이용하도록 한다.

Destruction

Destruction 단계에서는 BeanFactory와 ApplicationContext가 동일하게 동작한다.


다음은 destroy-method 속성이 정의된 applicationContext-user-bean.xml 의 일부이다.
<bean id="com.sds.emp.user.services.UserServiceWithIoCExtended"
	class="com.sds.emp.user.services.impl.UserServiceImplWithIoCExtended" 
	init-method="userInitialize" destroy-method="userDestroy" parent="parent">
</bean>
모든 Bean의 소멸자 method 설정은 <beans>의 default-destroy-method 속성을 이용한다.

Bean 상속

Bean 정의는 여러 속성 정보들, 생성자 인자, Property 값을 포함하여 많은 양의 설정 정보를 포함한다. 자식 Bean은 부모 정의로부터 설정 정보를 상속하여 정의한다. 그러므로 값을 오버라이드하거나 다른 것을 추가할 수 있다. 상속 관계를 이용하여 Bean을 정의하는 것은 XML 파일의 양을 줄일 수 있으므로 템플릿 형태의 부모 Bean을 정의하는 것은 유용하다. XML 기반의 속성 정의시 자식 Bean은 부모 Bean을 명시하기 위해 'parent' 속성을 사용해야 한다.
  • 부모 Bean 정의
  • 특수 설정 없이 부모 Bean으로 사용이 가능하며 class 속성 값을 설정하지 않은 경우, 반드시 abstract 속성 값을 "true"로 설정한다. abstract 속성 값이 "true"인 경우 Bean의 인스턴스화가 불가능하다.
  • 자식 Bean 정의
  • parent 속성 값에 부모 Bean의 id 혹은 name을 설정한다.
다음은 Bean 상속이 표현되어 있는 applicationContext-user-bean.xml 의 일부이다.
<!-- register parent bean that has a dependency with userDAO bean -->
<bean id="parent" abstract="true">
	<property name="userDAO" ref="userDAO" />	
</bean>

<bean id="com.sds.emp.user.services.UserServiceWithIoCExtended"
	class="com.sds.emp.user.services.impl.UserServiceImplWithIoCExtended" 
	init-method="userInitialize" destroy-method="userDestroy" parent="parent">
</bean>

Container 확장

Bean 후처리

Bean의 LifeCycle 중 Initialization 단계에서 Bean 초기화 시점 전후에 수행되는 것을 Bean 후처리라고 하며, BeanPostProcessor를 구현하면 기능을 확장할 수 있다. ApplicationContext 유형의 Container 사용 시에는 XML 파일에 BeanPostProcessor 인터페이스를 구현한 클래스를 등록만 시키면 Container가 해당 클래스를 BeanPostProcessor로 인식하여 각각의 Bean을 초기화하기 전과 후에 후처리 메소드를 호출해준다. 그러나 BeanFactory 유형의 Container를 사용하고 있다면 BeanFactory의 addBeanPostProcessor() 메소드를 이용하여 프로그램 상에서 등록해야 한다. 예시는 다음과 같다.
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
  // simply return the instantiated bean as-is
  public Object postProcessBeforeInitialization(Object bean, String beanName) 
                                                                     throws BeansException {
     return bean; // we could potentially return any object reference here...
  }
  public Object postProcessAfterInitialization(Object bean, String beanName) 
                                                                     throws BeansException {
     System.out.println("Bean '" + beanName + "' created : " + bean.toString());
     return bean;
  }
}
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>

BeanFactory 후처리

BeanFactoryPostProcessor를 구현하여 BeanFactory 후처리 기능을 확장할 수 있다. 모든 Bean에 대한 정의가 로딩된 후, BeanPostProcessor Bean을 포함한 어떤 Bean이라도 인스턴스화되기 이전에 Spring Container에 의해 BeanFactoryPostProcessor의 postProcessBeanFactory() 메소드가 호출된다.
따라서, BeanFactoryPostProcessor 인터페이스를 구현한 클래스 내에서 postProcessBeanFactory 메소드를 작성하고, Bean으로 정의하면 된다. 예시는 다음과 같다.
public class BeanCounterBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
	public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) 
                                                     throws BeansException {
     …중략
}
<bean class="test.BeanCounterBeanFactoryPostProcessor"/>
BeanFactoryPostProcessor는 BeanFactory 유형의 Container와 함께 사용될 수 없다. 유용한 BeanFactoryPostProcessor 구현 클래스는 PropertyPlaceholderConfigurer와 CustomEditorConfigurer이다.

다음은 PropertyPlaceholderConfigurer와 CustomEditorConfigurer에 대한 사용 예이다.
  • 설정 정보의 외부화
  • PropertyPlaceholderConfigurer를 사용하여 하나 이상의 외부 Property 파일로부터 속성들을 로딩하고 그 속성들을 이용하여 Bean 정의 XML 파일에서의 위치소유자(placeholder) 변수들을 채운다.
    다음은 설정 정보 외부화를 위해 PropertyPlaceholderConfigurer 클래스를 Bean으로 등록하고 있는 applicationContext-user-container.xml 의 속성 정의 부분이다.
    <!-- set file locations --> 
    <bean id="configurer" 
      class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
      <property name="locations">
    		<list>
            	<value>userConfigurer.txt</value>
    		</list>
    	</property>
    </bean>
     
    <bean id="com.sds.emp.user.services.UserServiceWithIoCExtended"
    	class="com.sds.emp.user.services.impl.UserServiceImplWithIoCExtended">
    	<property name="userDAO" ref="userDAO" />
    	<!-- set userCompany value using key name in properties file -->
    	<property name="userCompany" value="${user.company}"></property>
    </bean>
    위에서 외부 파일로 정의된 userConfigurer.txt 의 내용은 다음과 같다.
    user.company=SamsungSDS
  • PropertyEditor 확장
  • CustomEditorConfigurer를 사용하여 java.beans.PropertyEditor의 커스텀 구현 클래스를 등록하여 특성 값을 다른 특성 타입으로 번역할 수 있도록 한다. 확장한 PropertyEditor 클래스를 속성 정의 파일에 등록 후 PropertyEditor로 사용한다.
    <bean id="customEditorConfigurer"          
    	class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    	<property name="customEditors">
    	  <map>
    	    <entry key="com.springinaction.knight.PhoneNumber">
    	      <bean id="phoneEditor"          
    	         class="com.springinaction.springcleaning.PhoneNumberEditor" />
    	    </entry>
    	  </map>
    	</property>
    </bean>
    <bean id="knight" class="com.springinaction.knight.KnightOnCall">
       <property name="url" value="http://www.knightoncall.com" />
       <property name="phoneNumber" value="940-555-1234" />
    </bean>

ApplicationContext 활용

MessageSource를 활용한 국제화(I18N) 지원

ApplicationContext 인터페이스는 MessageSource라고 불리는 인터페이스를 확장해서 메시징(국제화 지원)기능을 제공하며 HierarchicalMessageSource와 함께 구조적인 메시지를 분석하는 능력을 가진다. MessageSourceAware인터페이스를 구현하는 Bean은 ApplicationContext의 messageSource Bean을 사용할 수 있다.

다음은 applicationContext-user-container.xml 의 messageSource 속성 정의 부분이다.
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="basenames">
		<list><value>userMessages</value></list>
	</property>
</bean>
Resource Bundle 파일은 국제화 지원을 위해 Locale별 파일로 구성하며 위에서 참조하는 userMessages.properties 파일은 다음과 같다.
argument.required=The "{0}" argument is required.
또한 UserServiceImplWithIoCExtended.java 파일에 messageSource를 얻는 부분은 다음과 같이 구현되어 있다.
new String(messageSource.getMessage("argument.required", new Object[] {"userVO"}, Locale.KOREA)
                                                            .getBytes("8859_1"), "euc-kr")
messageSource 부분을 테스트 할수있는 ContainerTest.java 파일을 수행시키면 다음과 같은 message를 확인할 수 있다.
"userVO" 인자는 반드시 필요하다.

Event

ApplicationContext는 어플리케이션이 구동하는 동안 다수의 이벤트를 발생시킬 수 있으므로, Listener를 Bean으로 등록하게 되면, Container는 해당하는 Event가 발생하면 관련 Listener의 onApplicationEvent() 메소드를 호출한다.
  • Built-in Events
  • 이벤트
    설명
    ContextRefreshedEvent ApplicationContext가 초기화되거나 갱신(refresh)될 때 발생하는 이벤트
    - 여기서 초기화는 모든 Bean이 로드되고 Singleton Bean들은 미리 인스턴스화되며 ApplicationContext는 사용할 준비가 된다는 것을 의미함
    ContextClosedEvent ApplicationContext의 close()메소드를 사용하여 ApplicationContext가 종료될 때 발생하는 이벤트
    - 여기서 종료는 Singleton Bean들이 소멸(destroy)되는 것을 의미함
    RequestHandledEvent HTTP Request가 처리되었을 때 WebApplicationContext 내에서 발생하는 이벤트
    - 이 이벤트는 Spring의 DispatcherServlet을 사용하는 웹 어플리케이션에서만 적용 가능함


    ApplicationListener를 구현한 Listener의 예시는 다음과 같다.
    public class RefreshListener implements ApplicationListener {
        public void onApplicationEvent(ApplicationEvent evt) {
    		if (evt instanceof ContextRefreshedEvent) { 
    			// 중략 …
    		}
    	}
    }


    앞서 구현한 RefreshListener 클래스에 대한 속성 정의 예시는 다음과 같다.
    <bean id="refreshListener" class="sample.RefreshListener"/>
    
  • Custom Event 발생
  • 사용자 정의 Event를 직접 발생시키고 해당 Event 발생 시 처리될 수 있도록 Listener를 등록하는 것도 가능하다. Event Listening을 하기 위해서는 Listener 등록이 필요하다. 다음은 Listener Bean을 등록하는 applicationContext-user-container.xml 파일의 일부이다.
    <bean id="userEventListener" class="com.sds.emp.user.services.impl.UserEventListener"/>
    다음은 UserEventListener.java 의 일부로, Custom Event인 UserEvent를 처리하고 있음을 알 수 있다.
    public class UserEventListener implements ApplicationListener {
      public void onApplicationEvent(ApplicationEvent evt) {
        if (evt instanceof UserEvent) {
          UserEvent event = (UserEvent)evt;
        System.out.println("Received in UserEventListener : " + event.getUserMessage());
        }
      }
    }
    다음은 UserServiceImplWithIoCExtended.java 파일로, Custom Event인 UserEvent를 발생시키는 부분이다.
    this.ctx.publishEvent(new UserEvent(this,"new user is added successfully."));

BeanFactory와 ApplicationContext 특징 비교

Feature
BeanFactory
ApplicationContext
Bean instantiation/wiring
Yes
Yes
Automatic BeanPostProcessor registration
No
Yes
Automatic BeanFactoryPostProcessor registration
No
Yes
Convenient MessageSource access (for i18n)
No
Yes
ApplicationEvent publication
No
Yes

대부분의 전형적인 어플리케이션 구축 시에는 ApplicationContext 사용을 권장한다.

Resources

  • 다운로드
  • 샘플 테스트 코드를 포함하고 있는 anyframe-iocextendedtest-src.zip 파일을 다운받은 후, 테스트 환경 설정 을 참조하여 위에서 제시한 예제 코드를 실행해 볼 수 있다.
    Name
    Download
    anyframe-iocextendedtest-src.zip
    Download

  • 참고자료