Using FactoryBeans to create the ‘uncreatable’ 을 번역한 글입니다.

Carlos SanchezMarc Logemann의 글에 답하기 위해 FactoryBean의 개념과 다수의 FactoryBean이 어떻게 삶을 좀 더 편하게 해주는지 자세히 말해야겠다.

Spring의 FactoryBean은 어떤 종류의 객체라도 ApplicationContext에서 사용할 수 있도록 해준다는 의미에서 간접적인 계층을 추가하는 특별한 bean이다.

Setter-injection과 constructor-injection의 예

먼저 Spring이 객체를 생성하는 ‘평범한’ 방법을 보자. bean들은 보통 default 생성자를 사용하여 만들거나(나중에 setter이용하여 속성에 값을 넣는다.) 특정 XML을 사용하여 생성자의 인자를 사용하여 만든다. 첫 번째 경우는 ‘setter-injection’이라고 부르고 나중의 것은 ‘constructor-injection’이라고 한다. 이 두 방법을 잠시 살펴보자. 다음의 테스트용 bean이 있고 아래에는 두 개의 bean 설정이 있다.

public class TestBean {
  private String name;
  private int age;

  public TestBean() {
  }

  public TestBean(String name) {
    this.name = name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

# normalBeans1.xml
<bean id="setter" class="example.TestBean">
<property name="name" value="Alef [setter]"/>
</bean>

<bean id="constructor" class="example.TestBean">
  <constructor -arg value="Alef [constructor]"/>
</bean>

보시다시피 setter-injection과 constructor-injection을 사용하는 건 쉽다.

Setter-injection과 constructor-injection 섞어 쓰기

원한다면 setter-injection과 constructor-injection을 섞어서 사용할 수도 있다. 따라서 위와 같은 TestBean이 있을 때 다음과 같이 쓸 수 있다.

<bean id="mixed" class="example.TestBean">
  <constructor -arg value="Alef [mixed]"/>
<property name="age" value="27"/>
</bean>

위에 있는 설정은 Spring으로 하여금 먼저 ‘Alef’ 라는 인자를 받는 생성자를 사용하여 TestBean을 만들고 그 후에 age 속성에 값을 (setAge() 메소드를 호출해서)적용한다.

그럼 이제 모든 종류의 객체를 설정하고 생성할 수 있는 방법을 익혔다. 이제 일반적인 JavaBean의 생성자를 가진 객체를 만들 수도 있고 setter와 인자가 있는 생성자를 가지고 객체의 속성에 값을 지정할 수 있는 객체를 만들 수도 있다.

Factory에서 만들어지는 객체와 다른 타입의 객체들

그러나 잠깐만 멈춰봐라! 만약에 객체가 public 생성자를 가지고 있지 않은 경우는 어떤가. 위에서 봤던 방식으로는 도무지 이런 객체를 생성할 방법이 없다. 그리고 또 만약에 어떤 객체가 생성자나 Setter를 가지고는 할 수 없는 부가적인 설정이 필요하다면 역시 객체를 생성할 수 없다.

그래서 FactoryBean이 나타난 것이다!

Spring의 FactoryBean은 그들 스스로가 factory가 되어 BeanFactory에서 사용될 객체들이 구현할 인터페이스다. 만약에 어떤 bean이 FactoryBean 인터페이스를 구현했다면 그 객체는 bean이 아닌 factory로 사용된다.

FactoryBean은 매우 단순한 인터페이스로 세 개의 메소드가 있다:

•   Class getObjectType() – factory가 반환하는 객체의 타입을 알려준다.
•    Boolean isSingleton() – factory가 늘 같은 객체를 반환하는지 아니면 getObject()를 호출 할 때 마다 다른 객체를 호출하는지 알려준다.
•   Object getObject() – 객체를 (만들어) 준다.

간단한 FactoryBean의 구현을 살펴보자.

public class SimpleFactoryBean implements FactoryBean {

  public Class getObjectType() {
    return TestBean.class;
  }

  public boolean isSingleton() {
    return false;
  }

  public Object getObject() {
    return new TestBean("Alef [factory]");
  }
}

# factoryBean2.xml
<bean id="simpleFactoryBean"
  class="example.SimpleFactoryBean"/>

FactoryBean이 실제 어떻게 동작하는지 설명하기 위해서 ApplicationContext에서 bean을 가져오는 API를 사용하겠다. 먼저 ClassPathXmlApplicationContext 클래스를 사용해서 설정 파일들을 읽어 들인다.

ApplicationContext ctx = new ClassPathXmlApplicationContext(
  new String[] {
    "normalBeans1.xml",
    "normalBeans2.xml",
    "factoryBeans1.xml" });

getBean(String name) 메소드를 사용해서 ApplicationContext에서 bean을 가져 올 수 있다. 아래 코드는 예상대로 TestBean을 반환한다.

TestBean tb = (TestBean)ctx.getBean("setter");
assertEquals("Alef [setter]", tb.getName());
tb = (TestBean)ctx.getBean("constructor");
assertEquals("Alef [constructor]", tb.getName());
tb = (TestBean)ctx.getBean("mixed");
assertEquals("Alef [mixed]", tb.getName());

그러나 –이제 FactoryBean이 나올 차례다.- 아래의 코드도 SimpleFactoryBean이 아닌 TestBean을 반환한다. 이전에 정의한 getObejct()에서 반환하도록 되어 있는 객체가 넘어온 것이다.

TestBean tb = (TestBean)ctx.get("simpleFactoryBean");
assertEquals("Alef [factory]", tb.getName());

좀 더 쓸만한 상황

물론 SimpleFactoryBean을 사용해서 TestBean 객체를 가져 오는 것은 너무 단순한 예제이기 때문에 쓸모 없어 보인다. 하지만 어떤 경우에는 오직 FactoryBean만이 극심한 혼란을 없앨 수 있는 방법이 된다. 이제부터 그런 상황들을 살펴보자.

‘희한한’ 곳에서 객체 얻어 오기

DataSources 같은 객체를JNDI에서 가져온다고 생각해보자. DataSource가 JNDI에 설정이 되어 있길 원하기 때문에 그냥 ‘new’를 사용할 수 있다. 따라서 새로운 InitialContext를 만들고 lookup()메소드를 사용해서 JNDI 트리로부터 명시적으로 가져와야 한다. 우린 여전히 DataSource가 Spring의 설정 파일을 통해 DI되길 원하기 때문에 간접적인 계층이 필요하다. FactoryBean이 그 해답을 제공한다. 아래의 FactoryBean을 보자. org.springframework.jndi.JndiObjectFactoryBean을 단순하게 만든 버전이라고 생각하면 된다.

public class JndiObjectFactoryBean implements FactoryBean {

  private String jndiName;

  public void setJndiName(String jndiName) {
    this.jndiName = jndiName;
  }

  public Object getObject() {
    // let's not worry about exceptions
    // and closing the context for now
    Context ctx = new InitialContext();
    return ctx.lookup(jndiName);
  }
}

아래의 XML 몇 조각을 사용하면 DataSource를 묶을 수 있고 다른 bean들이 다시 resort하거나 lookup할 필요 없이 참조할 수 있다. 다시 말하자면 DataSource를 DI할 수 있다! 이 예제는 FactoryBean을 설정해 두었기 때문에 JNDI 위치를 하드코딩 할 필요가 없다. 여기서 DataSource를 설정한 것이 아니라 FactoryBean을 설정했다는 것을 기억하라.

# jndiFactory.xml
<bean class="example.MyDao">
<property name="dataSource ref="myDataSource"/>
</bean>

<bean id="myDataSource" class="example.JndiObjectFactoryBean">
<property name="jndiName"
    value="java:comp/env/jdbc/MyDataSource"/>
</bean>

JavaBeans spec에 맞지 않는 객체

Property 객체를 생각해 보자. Spring에서는 속성들을 <props /> 태그를 사용해서 설정할 수 있다. TestBean 클래스에 Property 객체와 setter를 추가한다면 다음처럼 묶을 수 있다.

<bean class="example.TestBean">
  <!-- results in a call to setPropertiesSetter(props) -->
<property name="propertiesSetter">
<props>
      myName=Alef
      yourName=reader
    </props>
  </property>
</bean>

쉽게 이런 방법을 생각할 수 있다. 하지만 한 가지 단점이 있는데 만약 저기 있는 Property가 하나의 bean이 아닌 다른 여러 곳에서 필요하다면 어떨까. 일반적인 bean 처럼 <ref bean=”xxx”/> 할 수는 없다. In other words, the Properties object passed to the TestBean isn’t a first-class citizen in our Spring application context. 또한 <props /> 엘리먼트를 루트 레벨로 올릴 수도 없다. 또 다시 FactoryBean이 해답이 된다. 아래의 PropertiesFactoryBean(이것도 Spring에 있는 것을 단순하게 만든 버전이다.) 코드를 보자.

public class PropertiesFactoryBean implements FactoryBean {

  private Properties props;

  public void setProperties(Properties props) {
    this.props = props;
  }

  public Object getObject() {
    Properties p = new Properties(props);
    return p;
  }
}

이제 최상위 레벨의 Property 객체를 정의할 수 있게 됐고 다른 Bean들에도 DI할 수 있다.

<bean class="example.TestBean">
  <!-- results in a call to setPropertiesSetter(props) -->
<property name="propertiesSetter" ref="properties/>
</bean>
<bean id="properties" class="example.PropertiesFactoryBean">
<property name="properties">
<props>
      myName=Alef
      yourName=reader
    </props>
  </property>
</bean>

Spring이 제공하는 FactoryBean 에 대한 짧은 리뷰

살펴본 대로, FactoryBean은 일반적인 Spring DI container의 특성으로는 생성하기 힘든 객체를 DI할 때 매우 유용한 중간 계층을 제공한다. FactoryBean는 새로운 InitialContext 객체를 생성하거나 Property를 읽어 들이는 등의 너저분한 코드 작성을 없애준다. Spring을 이미 우리가 살펴본 PropertiesFactoryBean과 JndiObjectFactoryBean같은 유용한 FactoryBean들을 제공하고 있다. 아래에 조그만 설명화 함께 있는 리스트 들이 Spring이 제공하는 다른 FactoryBean들이다.

•    JndiObjectFactoryBean - retrieves JNDI objects from a JNDI context
•    ListFactoryBean - creates List instances (as first-class citizens in a Spring context) where you have the option to specify the type of List you want to use
•    MapFactoryBean - same as the ListFactoryBean but this one creates Maps
•    SetFactoryBean - idem
•    TimerFactoryBean - creates java.util.Timer objects and takes care of all the necessary configuration
•    ServletContextAttributeFactoryBean - retrieves attributes from a ServletContext (works in web-app contexts only
•    WebSphereTransactionManagerFactoryBean - retrieves the WebSphere transaction manager (uses WebSphere proprietary APIs)
•    WebLogicServerTransactionManagerFactoryBean - retrieves the WebLogic transaction manager
•    And many more

ps : 끄트머리는 귀찮아서 번역 안했습니다.;;;