143. 팩토리 메서드와 팩토리 빈 (2)
2. 인스턴스 팩토리 메서드를 이용한 간접 객체 생성
이번 실습에서는 인스턴스 팩토리 메서드를 정의하고 사용하는 방법을 배워본다.
- 실습 패키지 생성
exam 패키지 아래에 test12 패키지를 생성한다. test11 패키지의 모든 파일을 복사해온다.
- 팩토리 클래스 생성
exam.test12 패키지에 복사해 온 TireFactory 클래스에서 모든 메서드의 static 선언을 제거한다. 즉 모든 메서드를 인스턴스 메서드로 만든다.
package exam.test12;
import java.text.SimpleDateFormat;
import java.util.Properties;
public class TireFactory {
/**
* 팩토리 메서드
* @param maker
* @return createHankookTire(), createKumhoTire()
*/
public Tire createTire(String maker) {
if(maker.equals("Hankook")) {
return createHankookTire();
}else {
return createKumhoTire();
}
}
/**
* Hankook 타이어 생성
* @return tire
*/
private Tire createHankookTire() {
Tire tire = new Tire();
tire.setMaker("Hankook");
Properties specProp = new Properties();
specProp.setProperty("width", "205");
specProp.setProperty("ratio", "65");
specProp.setProperty("rim.diameter", "14");
tire.setSpec(specProp);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
tire.setCreatedDate(dateFormat.parse("2014-5-5"));
}catch (Exception e) {}
return tire;
}
/**
* Kumho 타이어 생성
* @return tire
*/
private Tire createKumhoTire() {
Tire tire = new Tire();
tire.setMaker("Kumho");
Properties specProp = new Properties();
specProp.setProperty("width", "185");
specProp.setProperty("ratio", "75");
specProp.setProperty("rim.diameter", "16");
tire.setSpec(specProp);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
tire.setCreatedDate(dateFormat.parse("2014-3-1"));
}catch (Exception e) {}
return tire;
}
}
- beans.xml 빈 설정 파일
인스턴스 메서드로 변경한 팩토리 메서드를 사용하려면 다음과 같이 설정한다.
exam/test12/beans.xml 파일을 열고 다음과 같이 편집한다.
<?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:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="tireFactory" class="exam.test12.TireFactory"/>
<bean id="hankookTire" factory-bean="tireFactory" factory-method="createTire">
<constructor-arg value="Hankook" />
</bean>
<bean id="kumhoTire" factory-bean="tireFactory" factory-method="createTire">
<constructor-arg value="Kumho" />
</bean>
</beans>
<bean id="tireFactory" class="exam.test12.TireFactory"/>
▶ 자바 코드로 표현하면?
TireFactory tireFactory = new TireFactory();
먼저 팩토리 클래스의 인스턴스를 생성한다. createTire() 메서드가 인스턴스 메서드가 되었기 때문에 이 메서드를 호출하려면 TireFactory 객체가 필요하다.
<bean id="hankookTire" factory-bean="tireFactory" factory-method="createTire">
<constructor-arg value="Hankook" />
</bean>
▶ 자바 코드로 표현하면?
Tire hankookTire = tireFactory.createTire("Hankook");
factory-bean 속성에 팩토리 객체(tireFactory)를 지정한다. class 속성은 설정하지 않는다. factory-method 속성에는 인스턴스 팩토리 메서드 이름을 지정한다. 스태틱 메서드는 안된다. 팩토리 메서드의 매개변수 값은 이전과 마찬가지로 <constructor-arg> 태그로 지정한다.
- 빈 컨테이너 테스트
인스턴스 팩토리 메서드를 통해 생성된 빈을 확인해 본다. test11 패키지에서 복사해 온 테스트 클래스 exam.test12.Test 클래스를 다음과 같이 편집한다.
package exam.test12;
import java.util.Map.Entry;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
//IoC 컨테이너 준비하여 빈 생성
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("exam/test12/beans.xml");
Tire t1 = (Tire)ctx.getBean("hankookTire");
Tire t2 = (Tire)ctx.getBean("kumhoTire");
Tire t3 = (Tire)ctx.getBean("hankookTire");
System.out.println("t1-->" + t1.toString());
System.out.println("t2-->" + t2.toString());
System.out.println("t3-->" + t3.toString());
if(t1 != t2) {
System.out.println("t1 != t2");
}
if(t1 == t3) {
System.out.println("t1 == t3");
}
}
}

Test 클래스에서 빈 설정 파일의 폴더 이름만 test11에서 test12로 바꾼다. 나머지 코드는 그대로 둔다. 실행 결과는 이전과 같다(그림 1).
3. 스프링 규칙에 따라서 팩토리 빈 만들기
이번 실습에서는 스프링 프레임워크에서 제공하는 규칙(인터페이스)에 따라 팩토리 클래스를 만들고 사용하는 방법을 배워본다. 이 방식으로 팩토리 빈을 만들면 스프링 IoC 컨테이너에서만 사용할 수 있다는 단점이 있지만, 스프링이 팩토리 빈을 다루는 방법을 이해하는데 많은 도움이 될 것이다.
- 실습 패키지 생성
exam 패키지 아래에 test13 패키지를 생성한다. test12 패키지의 모든 파일을 복사해온다.
- 팩토리 클래스 생성
스프링에서는 팩토리 빈이 갖추어야 할 규칙을 org.springframework.beans.factory.FactoryBean 인터페이스에 정의하였다. 팩토리 클래스를 만들 때 이 규칙에 따라 메서드를 구현하면 된다(그림 2).

보통 FactoryBean 인터페이스를 직접 구현하기보다는 스프링에서 제공하는 추상 클래스를 상속해서 만든다. org.springframework.beans.factory.config.AbstractFactoryBean 추상 클래스는 FactoryBean 인터페이스를 미리 구현하였다(그림 3).

AbstractFactoryBean 추상 클래스에는 createInstance()라는 추상 메서드가 있다. 빈을 생성할 때 팩토리 메서드로서 getObject() 메서드가 호출되는데, getObject() 메서드는 내부적으로 바로 이 메서드를 호출한다. 즉 실제 빈을 생성하는 것은 createInstance() 메서드이다. 따라서 AbstractFactoryBean 추상 클래스를 상속받을 때는 반드시 이 메서드를 구현해야 하고, 이 메서드에 빈 생성 코드를 넣는다.
여기에 하나 더 FactoryBean 인터페이스로부터 받은 getObjectType() 추상 메서드가 구현되지 않은 채로 남아 있다. 이 메서드는 팩토리 메서드(getObject())가 생성하는 객체의 타입을 알려준다. 이 메서드도 반드시 구현해야 한다.
exam.test12 패키지에 복사해 온 TireFactory 클래스의 이름을 TireFactoryBean 이라고 변경한다. 스프링에서는 보통 팩토리 빈의 이름을 XXXFactoryBean 이라고 짓는다. 스프링 코딩 관례에 맞춘다. 클래스의 내용은 다음과 같이 편집한다.
package exam.test13;
import java.text.SimpleDateFormat;
import java.util.Properties;
import org.springframework.beans.factory.config.AbstractFactoryBean;
public class TireFactoryBean extends AbstractFactoryBean<Tire> {
String maker;
public void setMaker(String maker) {
this.maker = maker;
}
/**
* AbstractFactoryBean으로부터 상속받은 추상 메서드
* @return exam.test13.Tire.class
*/
@Override
public Class<?> getObjectType() {
return exam.test13.Tire.class;
}
/**
* 팩토리 메서드 - AbstractFactoryBean으로부터 상속받은 추상 메서드
* @return createHankookTire(), createKumhoTire()
*/
protected Tire createInstance() {
if(maker.equals("Hankook")) {
return createHankookTire();
}else {
return createKumhoTire();
}
}
/**
* Hankook 타이어 생성
* @return tire
*/
private Tire createHankookTire() {
Tire tire = new Tire();
tire.setMaker("Hankook");
Properties specProp = new Properties();
specProp.setProperty("width", "205");
specProp.setProperty("ratio", "65");
specProp.setProperty("rim.diameter", "14");
tire.setSpec(specProp);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
tire.setCreatedDate(dateFormat.parse("2014-5-5"));
}catch (Exception e) {}
return tire;
}
/**
* Kumho 타이어 생성
* @return tire
*/
private Tire createKumhoTire() {
Tire tire = new Tire();
tire.setMaker("Kumho");
Properties specProp = new Properties();
specProp.setProperty("width", "185");
specProp.setProperty("ratio", "75");
specProp.setProperty("rim.diameter", "16");
tire.setSpec(specProp);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
try {
tire.setCreatedDate(dateFormat.parse("2014-3-1"));
}catch (Exception e) {}
return tire;
}
}
public class TireFactoryBean extends AbstractFactoryBean<Tire> {
...
}
TireFactoryBean 클래스는 AbstractFactoryBean 추상 클래스를 상속받는다. AbstractFactoryBean 추상 클래스는 하나의 코드로 다양한 타입의 빈을 다룰 수 있도록 제너릭(generic) 문법이 적용된 클래스이다. 이 클래스를 상속받을 때 타입 매개변수 T(AbstractFactoryBean<T>)에 들어갈 클래스 타입을 지정한다. 즉 팩토리 빈이 생성해 줄 인스턴스의 타입을 지정한다. 예제에서는 Tire 객체로 지정하였다.
String maker;
public void setMaker(String maker) {
this.maker = maker;
}
제조자(maker) 정보를 보관할 프로퍼티를 위해 셋터와 인스턴스 변수를 정의하였다. maker 프로퍼티는 팩토리 빈에서 객체를 생성할 때 참고할 것이다.
@Override
public Class<?> getObjectType() {
return exam.test13.Tire.class;
}
AbstractFactoryBean 추상 클래스로부터 상속받은 추상 메서드 getObjectType() 메서드를 구현한다. 반환값은 팩토리 빈이 만드는 객체의 타입이다. TireFactoryBean 클래스는 Tire 객체를 생성해주는 팩토리 빈이다.
protected Tire createInstance() {
if(maker.equals("Hankook")) {
return createHankookTire();
}else {
return createKumhoTire();
}
}
상속받은 또 하나의 추상 메서드 createInstance() 메서드를 구현한다. 메서드의 내부 코드는 이전과 같다. 여기서 주목할 것은 접근 제어자이다. public이 아니라 protected로 되어 있다. 직접 사용하는 메서드가 아니라 getObject() 메서드에 의해 내부적으로 호출되는 메서드이다. 이런 이유로 이 메서드는 AbstractFactoryBean 추상 클래스에서 protected로 선언되어 있다. 나머지 메서드는 이전 코드와 같다.
- beans.xml 빈 설정 파일
스프링 규칙에 따라 만든 팩토리 빈은 다음과 같이 설정하여 사용한다.
exam/test13/beans.xml 파일을 열고 다음과 같이 편집한다.
<?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:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="hankookTire" class="exam.test13.TireFactoryBean">
<property name="maker" value="Hankook"/>
</bean>
<bean id="kumhoTire" class="exam.test13.TireFactoryBean">
<property name="maker" value="Kumho"/>
</bean>
</beans>
<bean id="hankookTire" class="exam.test13.TireFactoryBean">
<property name="maker" value="Hankook"/>
</bean>
▶ 자바 코드로 표현하면?
FactoryBean tempFactoryBean = new TireFactoryBean();
tempFactoryBean.setMaker("Hankook");
Tire tireFactory = tempFactoryBean.getObject();
스프링에서 제공하는 규칙에 따라 만든 팩토리 빈은 사용법이 이전과 비슷하다. 다만, 팩토리 역할을 할 메서드를 지정할 필요가 없다. 스프링 IoC 컨테이너는 class 속성에 주어진 클래스가 일반 클래스가 아니라 FactoryBean 타입의 클래스일 경우, 이 클래스의 인스턴스를 직접 보관하는 것이 아니라 이 클래스가 생성한 빈을 컨테이너에 보관한다. 즉 TireFactoryBean 클래스는 FactoryBean 타입이기 때문에 이 객체를 직접 보관하는 것이 아니라 TireFactoryBean 클래스가 생성한 Tire 객체를 'hankookTire'라는 이름으로 컨테이너에 보관한다. 이것을 UML 시퀀스 다이어그램으로 표현해본다(그림 4).

스프링 IoC 컨테이너는 팩토리 빈(TireFactoryBean)에게 인스턴스를 만들어 달라고 요청(getObject() 호출)한다. 팩토리 빈은 우리가 구현한 createInstance() 메서드를 호출한다. 그리고 createInstance() 메서드가 생성해 준 인스턴스를 그대로 컨테이너에게 반환한다.
- 빈 컨테이너 테스트
스프링 규칙에 따라서 만든 TireFactoryBean 클래스를 테스트해 본다. test12 패키지에서 복사해 온 테스트 클래스 exam.test13.Test 클래스를 다음과 같이 편집한다.
package exam.test13;
import java.util.Map.Entry;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
//IoC 컨테이너 준비하여 빈 생성
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("exam/test13/beans.xml");
Tire t1 = (Tire)ctx.getBean("hankookTire");
Tire t2 = (Tire)ctx.getBean("kumhoTire");
Tire t3 = (Tire)ctx.getBean("hankookTire");
System.out.println("t1-->" + t1.toString());
System.out.println("t2-->" + t2.toString());
System.out.println("t3-->" + t3.toString());
if(t1 != t2) {
System.out.println("t1 != t2");
}
if(t1 == t3) {
System.out.println("t1 == t3");
}
}
}
Test 클래스에서 빈 설정 파일의 폴더 이름만 test12에서 test13으로 바꾼다. 나머지 코드는 그대로 둔다. 실행 결과는 이전과 같다(그림 5).

참고도서 : https://freelec.co.kr/book/1674/
[열혈강의] 자바 웹 개발 워크북
[열혈강의] 자바 웹 개발 워크북
freelec.co.kr