Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 4장 IoC 컨테이너 #8

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.




4.8 컨테이너 확장점(Extension Points)

보통 어플리케이션 개발자는 ApplicationContext 구현 클래스들의 서브클래스를 만들 필요가 없다. 대신 특수한 통합 인터페이스의 구현을 연결해서 스프링 IoC 컨테이너를 확장할 수 있다.

4.8.1 BeanPostProcessor를 사용한 빈 커스터마이징

BeanPostProcessor 인터페이스는 개발자가 원하는(또는 컨테이너의 기본로직을 오버라이드하는) 인스턴스화 로직, 의존성 처리로직 등을 구현할 수 있는 callback methods를 정의한다. 스프링 컨테이너가 인스턴스화, 설정, 빈 초기화 작업을 끝낸 뒤 어떤 커스텀 로직을 구현하려면 하나 이상의 BeanPostProcessor 구현체를 연결할 수 있다.

여러 개의 BeanPostProcessor 인스턴스를 설정할 수 있고 order 프로퍼티를 통해 이러한 BeanPostProcessor의 실행순서를 제어할 수 있다. 이 order 프로퍼티는 BeanPostProcessor가 Ordered 인터페이스를 구현했을 때만 설정할 수 있다. 직접 BeanPostProcessor를 작성한다면 Ordered 인터페이스도 같이 구현해야 한다. 더 자세한 정도는 BeanPostProcessor 인터페이스와 Ordered 인터페이스의 JAVADOC을 참고해라. 또한 BeanPostProcessors의 프로그래밍적인 등록을 참고해라.

Note
BeanPostProcessor는 빈(또는 객체) 인스턴스상에서 동작한다. 즉, 스프링 IoC 컨테이너는 빈 인스턴스를 인스턴스화한 다음에 BeanPostProcessor가 자신의 일을 수행한다.

BeanPostProcessor는 컨테이너에 한정된 (per-container) 범위를 갖는데 이는 컨테이너 계층(hierarchies)을 사용할 때만 적절한 표현이다. 하나의 컨테이너에서 BeanPostProcessor를 정의한다면 BeanPostProcessor는 해당 컨테이너의 빈들에 대한 후처리(post-process) 만 할 것이다. 다시 말하면 한 컨테이너에서 정의된 빈들은 다른 빈에서 정의된 BeanPostProcessor가 후처리를 하지 않고 두 컨테이너가 같은 계층에 속해있더라도 마찬가지이다.

실제 빈 정의를 바꾸려면(예를 들어 빈을 정의하는 청사진(blueprint)) Section 4.8.2, “BeanFactoryPostProcessor로 설정 메타데이터 커스터마이징하기”에서 설명했듯이 BeanFactoryPostProcessor를 사용해야 한다.

org.springframework.beans.factory.config.BeanPostProcessor 인터페이스는 정확히 2개의 콜백 메서드로 구성되어 있다. 이러한 클래스를 컨테이너의 후처리자(post-processor)로 등록했다면 컨테이너가 생성한 각 빈 인스턴스에 대해서 후처리자는 컨테이너 초기화 메서드(InitializingBean의 afterPropertiesSet()와 선언된 init 메서드같은)가 호출되기 이전뿐만 아니라 빈 초기화 콜백 이후에 대해 컨테이너로부터 콜백을 얻는다. 후처리자는 콜백을 완전히 무시해버리는 것을 포함해서 빈 인스턴스에 대한 어떤 액션도 할 수 있다. 빈 후처리자는 보통 콜백 인터페이스를 확인하거나 빈을 프록시로 감쌀 것이다. 몇몇 스프링 AOP 인프라 클래스는 프록시-랩핑(proxy-wrapping) 로직을 제공하려고 빈 후처리자로 구현되었다.

ApplicationContext는 설정메타데이터에서 선언된 빈중에 BeanPostProcessor 인터페이스를 구현한 빈을 자동으로 찾아낸다. ApplicationContext는 이러한 빈을 빈 생성후에 호출될 수 있도록 후처리자로 등록한다.빈 후처리자는 다른 빈들처럼 컨테이너에 배포될 수 있다.

프로그래밍적인 BeanPostProcessors 등록
BeanPostProcessor 등록은 ApplicationContext의 자동탐지(위에서 설명했듯이)를 통한 접근을 권장하지만 addBeanPostProcessor 메서드를 사용해서 ApplicationContext에 프로그래밍적으로 BeanPostProcessor를 등록하는 것도 가능하다. 이는 등록하기 전에 조건을 평가해 볼 필요가 있거나 계층사이의 컨텍스트를 넘어서 빈 후처리자를 복사해야 할 때 유용하다. 하지만 프로그래밍으로 추가된 BeanPostProcessors는 Ordered 인터페이스를 고려해서는 안된다. 여기서는 등록하는 순서가 실행의 순서이가. 프로그래밍으로 등록된 BeanPostProcessors는 명시적인 순서에 상관없이 항상 자동탐지로 등록된 BeanPostProcessors보다 먼저 처리된다.

BeanPostProcessors와 AOP 자동 프록싱(auto-proxying)
BeanPostProcessor 인터페이스를 구현한 클래스들은 특별하고 컨테이너가 다르게 취급한다. 모든 BeanPostProcessors와 BeanPostProcessors가 직접 참조하는 빈들은 ApplicationContext의 특별한 시작 단계의 일부로 시작할 때 인스턴스화된다. 그 다음 모든 BeanPostProcessors가 정렬된 방식으로 등록되고 컨테이너의 모든 빈에 적용된다. AOP 자동 프록싱은 BeanPostProcessor 자체로 구현되었기 때문에 BeanPostProcessor들과 BeanPostProcessor들이 직접 참조하는 빈들 모두 자동 프록싱에 적합하지 않으므로 관점(aspect)을 위빙(weave)하지 말아야 한다.

이러한 빈에 대해서는 정보성 로그 메시지를 봐야한다. : “Bean foo is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)”.

다음 예제는 ApplicationContext에서 BeanPostProcessors를 어떻게 작성해서 등록하고 사용하는 지를 보여준다.

4.8.1.1 예제 : BeanPostProcessor 스타일의 Hello world
첫 예제는 기본적인 사용방법을 설명한다. 예제는 컨테이너가 생성한 각 빈의 toString() 메서드를 호출하고 시스템콘솔에 결과 문자열을 출력하는 커스텀 BeanPostProcessor 구현체를 보여준다.

다음 커스텀 BeanPostProcessor 구현클래스 정의를 보자.


package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

  // 간단히 현상태의 인스턴스화 된 빈을 리턴한다
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    return bean; // 여기서 잠재적으로 어떤 객체의 참조도 리턴할 수 있다...
  }

  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("Bean '" + beanName + "' created : " + bean.toString());
    return bean;
  }
}


<?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:lang="http://www.springframework.org/schema/lang"
     xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/lang
         http://www.springframework.org/schema/lang/spring-lang-3.0.xsd">

  <lang:groovy id="messenger"
        script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
      <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
  </lang:groovy>

  <!--
      위의 빈(messenger)가 인스턴스화 되었을 때 이 커스텀 
      BeanPostProcessor 구현체는 시스템콘솔에 결과를 출력할 것이다
   -->
  <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>


InstantiationTracingBeanPostProcessor를 얼마나 간단히 정의했는지 봐라. 그냥 다른 빈처럼 의존성 주입될 수 있는 빈이기 때문에 이름도 지정하지 않았다. (앞의 설정은 Grooby 스크립트가 지원하는 빈도 정의했다. 스프링 2.0의 동적언어 지원은 Chapter 27, Dynamic language support 챕터에서 자세히 설명한다.)

다음의 간단한 자바 어플리케이션의 앞의 코드와 설정을 실행한다.


import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {
  public static void main(final String[] args) throws Exception {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
    Messenger messenger = (Messenger) ctx.getBean("messenger");
    System.out.println(messenger);
  }
}


앞의 어플리케이션의 결과는 다음과 같다.


Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961



4.8.1.2 예제 : RequiredAnnotationBeanPostProcessor
보통 스프링 IoC 컨테이너를 확장하기 위해서 커스텀 BeanPostProcessor 구현체와 겹합된 어노테이션이나 콜백 인터페이스를 사용한다. 예제는 (임의의) 어노테이션으로 표시된 빈의 JavaBean 프로퍼티를 보장하는 스프링 배포판에 포함된 스프링의 RequiredAnnotationBeanPostProcessor ? BeanPostProcessor 구현체는 실제로 값으로 의존성 주입(되도록 설정)된다.

4.8.2 BeanFactoryPostProcessor로 설정 메타데이터 커스터마이징하기
이제 살펴보려는 다음 확장점은 org.springframework.beans.factory.config.BeanFactoryPostProcessor 이다. 이 인터페이스의 의미는 BeanPostProcessor가 빈 설정 메타데이터상에서 수행된다는 주요한 차이점을 제외하면 BeanPostProcessor와 유사하다. 이는 스프링 IoC 컨테이너가 BeanFactoryPostProcessors가 설정 메타데이터를 읽고 컨테이너가 BeanFactoryPostProcessors를 제외한 모든 빈을 인스턴스화하기 전에 바꿀 수 있도록 한다.

여러 BeanFactoryPostProcessors를 설정할 수 있고 order 프로퍼티를 설정해서 이 BeanFactoryPostProcessor들의 실행순서를 제어할 수 있다. 하지만 BeanFactoryPostProcessor가 Ordered 인터페이스를 구현했을 때만 order 프로퍼티를 설정할 수 있다. 직접 BeanFactoryPostProcessor를 작성한다면 Ordered 인터페이스도 구현하는 것을 고려해야 한다. 더 자세한 내용은 BeanFactoryPostProcessor와 Ordered 인터페이스의 Javadoc을 참고해라.

Note
실제 빈 인스턴스를 바꾸고 싶다면 (예를 들어 설정메타데이터로 생성된 객체들) 대신 BeanPostProcessor를 사용할 필요가 있다. (앞의 Section 4.8.1, “BeanPostProcessor를 사용한 빈 커스터마이징”에서 설명했다.) 이는 기술적으로 BeanFactoryPostProcessor내에서 빈 인스턴스들과 연동될 가능성이 있으므로 너무 이른 시기에 빈을 인스턴스화해서 표준 컨테이너 라이프사이클을 위반하게 한다. 이는 빈의 후처리를 우회하는 것과 같은 부정적인 효과를 일으킬 것이다.

또한, BeanFactoryPostProcessors는 컨테이너에 한정된(per-container) 범위를 가진다. 이는 컨테이너 계층을 사용할 경우에만 관련있다. 한 컨테이너에서 BeanFactoryPostProcessor을 정의한다면 해당 컨테이너의 빈 정의에만 적용될 것이다. 한 컨테이너의 빈 정의는 두 컨테이너가 같은 계층에 속해있더라도 다른 컨테이너의 BeanFactoryPostProcessors에 의해서는 후처리되지 않을 것이다.

빈 팩토리 후처리자(post-processor)는 컨테이너를 정의하는 설정 메타데이터를 수정하기 위해 ApplicationContext 내부에서 선언되었을 때 자동적으로 실행된다. 스프링은 PropertyOverrideConfigurer와 PropertyPlaceholderConfigurer같은 빈 팩토리 후처리자를 미리 다수 정의해 놓고 있다. 커스텀 프로퍼티 에디터를 등록하는 것 같은 커스텀 BeanFactoryPostProcessor도 사용할 수 있다.

ApplicationContext는 ApplicationContext내의 빈 중에서 BeanFactoryPostProcessor 인터페이스를 구현한 빈을 자동으로 감지한다. ApplicationContext는 적절한 시기에 이러한 빈들을 빈 팩토리 후처리자로 사용한다. 다른 빈처럼 이러한 후처리자 빈을 배포할 수 있다.

Note
BeanPostProcessor처럼 보통 BeanFactoryPostProcessor들을 지연 초기화되도록 설정하지 않는다. Bean(Factory)PostProcessor을 참조하는 다른 빈이 없다면 이 후처리자는 절대 인스터스화되지 않을 것이다. 그러므로 지연 초기화되도록 설정해도 이는 무시될 것이고 <beans /> 요소의 선언에서 default-lazy-init 속성을 true로 설정했더라도 Bean(Factory)PostProcessor는 인스터스화 되려고 할 것이다.


4.8.2.1 예제: PropertyPlaceholderConfigurer
표준 자바 Properties 형식을 사용하는 분리된 파일의 빈 정의에서 프로퍼티 값들을 구체화히하기 위해서 PropertyPlaceholderConfigurer를 사용한다. PropertyPlaceholderConfigurer를 사용하면 복잡합이나 핵심 XML 정의 파일이나 컨테이너에 대한 파일들을 수정하는 위험성없이 데이터베이스 URL과 비밀번호같은 환경의존적인 프로퍼티들을 커스터마이징 하기 위해 사람들이 어플리케이션을 배포할 수 있게 한다.

플레이스홀더 값들과 함께 DataSource가 정의된 다음의 XML 기반 설정 메타데이터의 일부를 보자. 예제는 외부 Properties 파일에서 설정되 프로퍼티들을 보여준다. 런타임에서 PropertyPlaceholderConfigurer는 DataSource의 몇몇 프로퍼티들을 교체할 메타데이터를 적용한다. 교체할 값들은 다음의 Ant / log4j / JSP EL 스타일의 ${property-name} 혁식으로 placeholders로 지정한다.


<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="locations" value="classpath:com/foo/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
    class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="${jdbc.driverClassName}"/>
  <property name="url" value="${jdbc.url}"/>
  <property name="username" value="${jdbc.username}"/>
  <property name="password" value="${jdbc.password}"/>
</bean>



실제 값들은 표준 자바 Properties 형식의 다른 파일에서 가져온다.



jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

그러므로 ${jdbc.username} 문자열은 런타임에서 'sa' 값으로 교체되고 프로퍼티 파일에서 키가 일치하는 다른 플레이스홀더 값들에도 똑같이 적용된다. PropertyPlaceholderConfigurer는 빈 정의에서 대부분의 프로퍼티와 속성의 플레이스 홀더를 확인한다. 게다가 플레이스홀더 접두사와 접미사는 커스터마이징 될 수 있다.

스프링 2.5에서 도입된 context 네임스페이스로 전용 설정 요소의 프로퍼티 플레이스홀더를 설정할 수 있다. location 속성에서 콤마로 분리된 리스트로 하나 이상의 위치를 제공할 수 있다.



<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>



PropertyPlaceholderConfigurer는 지정한 Properties 파일에서만 프로퍼티를 찾지 않는다. 지정한 프로퍼티 파일들에서 프로퍼티를 찾을 수 없다면 기본적으로 자바 System 프로퍼티들도 확인한다. 다음 지원하는 세가지 정수값 중 하나로 설정해서 이러한 동작을 수정할 수 있다.
  • never (0): 시스템 프로퍼티들을 확인하지 않는다
  • fallback (1): 지정한 프로퍼티 파일들에서 처리할 수 없으면 시스템 프로퍼티를 확인한다. 이 값이 기본값이다.
  • override (2): 지정한 프로퍼티 파일들보다 시스템 프로퍼티를 먼저 확인한다. 이는 시스템 프로퍼티가 다른 프로퍼티 소스를 오버라이드하도록 한다.
더 자세한 내용은 PropertyPlaceholderConfigurer의 Javadoc을 봐라.

클래스명 치환
런타임에서 특정 구현 클래스를 선택하는데 종종 유용한 클래스명을 치환에 PropertyPlaceholderConfigurer를 사용할 수 있다. 예를 들어

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="locations">
      <value>classpath:com/foo/strategy.properties</value>
  </property>
  <property name="properties">
      <value>custom.strategy.class=com.foo.DefaultStrategy</value>
  </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>


런타임에서 유효한 클래스를 찾지 못하면 지연초기화 되지 않는 빈에 대해 ApplicationContext의 preInstantiateSingletons() 단계에서 클래스가 생성될 때 빈 처리가 실패한다.


4.8.2.2 예제: PropertyOverrideConfigurer
또다른 빈 팩토리 후처리자인 PropertyOverrideConfigurer는 PropertyPlaceholderConfigurer와 비슷하지만 원래의 정의가 빈 프로퍼티에 대해 기본값을 갖거나 값을 갖지 않을 수 있다는 점이 다르다. 오버라이딩 된 Properties 파일에 어떤 빈 프로퍼티에 대한 진입점이 없다면 기본 컨텍스트 정의가 사용된다.

빈 정의는 오버라이드되었다는 것을 알지 못한다. 그래서 오버라이드 설정자를 사용하는 XML 정의에서 당장은 명확하지 않다. 같은 빈 프로퍼티에 대해 다른 값을 정의한 여러 PropertyOverrideConfigurer 인스턴스가 있는 경우 오버라이딩 메카니즘에 따라 마지막 PropertyOverrideConfigurer의 값을 사용한다.

프로퍼티 파일 설정은 다음과 같은 형식이다.

beanName.property=value

예를 들어

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

이 예제 파일은 driver와 url 프로퍼티를 가진 dataSource라는 빈을 담고있는 컨테이너 정의와 함께 사용된다.

오버라이드된 최종 프로퍼티를 제외한 경로의 모든 컴포넌트가 이미 null이 아니라면 프로퍼티 이름의 혼합도 역시 지원한다.(아마도 생성자에 의해서 초기화될 것이다.) 이 예제에서...

foo.fred.bob.sammy=123

... foo 빈의 fred 프로퍼티의 bob 프로퍼티의 sammy 프로퍼티는 123이라는 스칼라(scalar) 값으로 설정된다.

Note
지정한 오버라이드 값은 항상 리터럴(literal) 값이다. 이 값들은 빈 참조로 변환되지 않는다. 이 관례는 XML 빈 정의에서 원래의 값이 빈 참조를 지정한 경우에도 적용된다.

스프링 2.5에서 도입된 context 네임스페이스를 사용하면 전용 설정 요소로 설정 프로퍼티를 오버라이딩 하는 것이 가능하다.

<context:property-override location="classpath:override.properties"/>

4.8.3 FactoryBean로 인스턴스화(instantiation) 로직 커스터마이징하기
org.springframework.beans.factory.FactoryBean 인터페이스를 구현한 객체들은 그 자체로 팩토리이다.

FactoryBean 인터페이스는 스프링 IoC 컨테이너의 인스턴스화 로직에 플러그인할 수 있는 지점(a point of pluggability)이다. (잠재적으로) 장황한 XML과는 반대로 자바로 더 좋게 표현한 복잡한 인스턴스화 코드가 있다면 자신만의 FactoryBean를 생성해서 클래스에 복잡한 인스턴스화 로직을 작성한 뒤 컨테이너에 커스터마이징한 FactoryBean를 연결한다.

FactoryBean 인터페이스는 세 가지 메서드를 제공한다.

  • Object getObject(): 이 팩토리가 생성한 객체의 인스턴스를 리턴한다. 인스턴스는 이 팩토리가 싱글톤을 리턴하는지 프로토타입을 리턴하는지에 따라 공유될 수도 있다.
  • boolean isSingleton(): FactoryBean가 싱글톤을 리턴하면 true를 리턴하고 싱글톤이 아니면 false를 리턴한다.
  • Class getObjectType(): getObject() 메서드로 리턴되는 객체의 타입을 리턴한다. 타입을 미리 알고 있지 않다면 null을 리턴한다.
FactoryBean의 개념과 인터페이스는 스프링 프레임워크내 곳곳에서 사용된다. 스프링에는 FactoryBean 인터페이스의 구현체가 50개 이상 포함되어 있다.

빈이 생성한 것 대신 컨테이너에 실제 FactoryBean 인스턴스 자체를 요청할 필요가 있는 경우 ApplicationContext의 getBean() 메서드를 호출할 때 앤드기호(&)가 있는 빈의 id로 시작해라. 그래서 myBean이라는 id로 주어진 FactoryBean에 대해 컨테이너에서 getBean("myBean")를 호출하면 FactoryBean의 결과를 리턴한다. 반면에 getBean("&myBean")를 호출하면 FactoryBean 인스턴스 자체를 리턴한다.
2012/04/25 02:16 2012/04/25 02:16