어떻게 오브젝트 생성 시 필요한 초기화 정보의 획득과 입력 작업을 오브젝트 생성 로직에서 분리할 수 있을까?
일반적으로 오브젝트 생성과 관련하여 개발자들의 관심사는 Constructor, Builder, Factory 패턴 등과 같은 것이다. 개발자들은 오브젝트 생성을 위해 상황에 맞는 적절한 생성 패턴을 사용하여 오브젝트 생성 알고리즘을 구현한다.
이와 같이 오브젝트 생성과 관련해서는 잘 정리된 생성 패턴들이 있지만, 오브젝트 생성 시 필요한 초기화 정보 획득과 등록 작업에 관련된 적절한 패턴에 대한 고민은 상대적으로 부족하다. 그러나 프로그램을 개발하다 보면 오브젝트 생성을 위한 생성 패턴뿐만 아니라 초기화 정보의 획득 및 입력 작업에 대해 일관되고 확장이 용이한 방법에 대해서도 자주 고민하게 된다.
프로그램에서 오브젝트를 생성하기 위해서는 오브젝트 초기화에 필요한 입력 정보를 오브젝트에 입력해야 한다. 생성되는 오브젝트가 수행하는 작업에 따라 초기화에 사용되는 입력 정보는 통신 관련 오브젝트라면 원격지 접속 주소, 데이터베이스 처리 관련 오브젝트라면 데이터베이스 접속 정보, 데이터 파일 관리 오브젝트라면 데이터 파일 디렉터리 위치와 파일명 등이 될 수 있다. 그리고 생성되는 오브젝트의 초기화 입력 정보 형식에 따라 문자형, 숫자형과 같은 단순 원시 자료형에서 복합 구조의 오브젝트 형식 등 입력되는 정보의 형식도 다양한 구조를 가질 수 있다. 또한 생성되는 오브젝트의 초기화 입력 정보의 추출 경로에 따라 초기화 입력 정보는 하드 코딩, 설정 파일, 설정 데이터베이스, 설정 리지스트리, 원격지 설정 서버 등 다양한 경로를 통해 획득할 수 있다.
이와 같이 오브젝트 생성을 위한 초기화 정보 획득 및 입력 작업은 오브젝트 작업 내용, 초기화 정보 입력 형식, 초기화 정보 추출 경로에 따라 다양한 조합이 만들어져 개발에 따른 복잡성이 증가하고 일관성 있는 구현을 방해하게 된다. 이렇게 오브젝트 생성과 관련된 초기화 정보 획득개발이 복잡하고 일관성 없는 개발로 빠져들기 쉬운 문제점을 해결하기 위해 오브젝트 생성 로직에서 설정자(Configurer) 패턴을 사용할 수 있다.
설정자(Configurer) 패턴은 전략 패턴을 응용한 것으로 오브젝트 생성 시 오브젝트 생성 로직과 초기화 정보 획득 및 등록 로직을 분리하여 초기화 정보 획득 및 등록 작업에 일관성을 부여하고 초기화 정보 획득 및 등록 작업 알고리즘을 필요에 따라 대체할 수 있는 구조를 제공한다.
아래는 Factory 패턴과 함께 사용된 설정자(Configurer) 패턴 클래스 다이어그램이다. Factory 패턴 구조는 Design Patterns 의 Factory 패턴 구조이고 Design Patterns에서 제시한 Factory 패턴에서 생성에 필요한 로직은 Factory에 구현하고 Factory는 초기화 정보 획득과 입력 작업을 Configurer에 위임한다. 즉 Factory에서 초기화 정보 획득 및 입력을 분리하여 Configurer가 처리하도록 Factory 패턴의 역할을 나누고 확장한 것이다.
설정자(Configurer) 패턴 구조
Product
- Factory Method가 생성하는 오브젝트 인터페이스
ConcreteProduct
- Product 인터페이스 구현 클래스
- 생성 시 필요한 초기화 입력 Method 구현
Configurer
- 오브젝트 생성 시 오브젝트 초기화 정보 입력 Method 인터페이스
CustomConfiguer
- Configure 인터페이스 구현 클래스
- 오브젝트 생성관련 초기화 정보 추출 및 입력 처리, Setter Method구현 등
- configure Method에서 Product 구현 오브젝트에 초기화 정보 입력
Factory
- 오브젝트 생성 Method인 getObject 정의를 가진 Factory 인터페이스
ConcreteFactory
- Factory 구현 클래스
- Product 오브젝트 생성 시 Configurer 오브젝트에 초기화 정보 추출 및 입력 처리 위임
- 오브젝트 생성관련 준비 작업 및 생성 오브젝트 반환
설정자(Configurer) 패턴은 Factory 패턴뿐만 아니라 Builder 패턴에서도 적용할 수 있는데 Factory패턴에서와 마찬가지로 Builder에서 초기화 정보 입력을 Configurer 오브젝트에 위임하는 방식으로 구현하면 된다.
적용 사례
이제부터 설정자 패턴 사용을 실제 예를 통해 살펴보자. 설명을 위해 사용한 Factory 패턴은 Spring Framework의 FactoryBean이다. 즉 Spring Framework 내에서 FactoyBean 사용 시 설정자 패턴을 적용하는 예를 설명할 것이다.
먼저 Java 프로그램에서 오브젝트가 어떻게 생성되는지
ektorp 란 라이브러리에서 HttpClient 오브젝트를 생성하는 프로그램을 보자. 아래는 소스를 보면 ektorp 라이브러리는 HttpClient 오브젝트 생성을 위해 Builder 패턴을 사용하고 있다.
HttpClient 오브젝트 생성 예
HttpClient httpClient = new StdHttpClient.Builder()
.host("localhost")
.port(8080)
.build();
ektorp 라이브러리는 Builder 패턴을 사용하여 오브젝트를 생성하므로 구조적으로는 잘 만들어진 라이브러리로 볼 수 있다. 그러나 Spring Framework에서는 Bean 형식을 고려하지 않고 개발된 Java 오브젝트를 Spring Framework에서 직접 생성하기가 쉽지 않다. 이런 경우 Spring Framework는 임의의 Java 오브젝트를 생성하는 Spring Bean Factory 메커니즘을 제공한다. 다시 말해서 Spring Framework가 제공하는 Factory 패턴 방식을 사용하면 Bean 형식의 생성 초기화 방식을 갖지 않는 Java 클래스를 Spring Framework에서 사용할 수 있는 오브젝트로 생성할 수 있다. 아래와 같이 Spring Framework가 제공하는 Factory 인터페이스인 FactoryBean 인터페이스를 상속받아 HttpClientFactoryBean을 작성하면 httpClient 오브젝트를 생성할 수 있다.
HttpClientFactoryNoConfigurerBean.java
package com.brm.pattern.configurer;
import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.springframework.beans.factory.FactoryBean;
public class HttpClientFactoryNoConfigurerBean implements FactoryBean<HttpClient> {
private String host;
private int port;
public HttpClient getObject() throws Exception {
return new StdHttpClient.Builder()
.host(host)
.port(port)
.build();
}
public Class<? extends HttpClient> getObjectType() {
return StdHttpClient.class;
}
public boolean isSingleton() {
return true;
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
}
Spring Framework에서 HttpClientFactoryNoConfigurerBean 클래스는 아래 Spring XML 설정처럼 사용된다. 아래 Bean myHttpClientOrg 정의에서 생성되는 오브젝트는 HttpClientFactoryNoConfigurerBean의 getObject 메서드를 통해 생성된 오브젝트다.
application.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="myHttpClientOrg" class="com.brm.pattern.configurer.HttpClientFactoryNoConfigurerBean">
<property name="host" value="localhost" />
<property name="port" value="8080" />
</bean>
</beans>
위 Spring XML에서 Bean 정의에 오브젝트 생성을 위한 초기 입력값 host와 port 정보는 property로 정의에 속성값으로 주입하였다. 속성 주입은 Spring Framework의 가장 중요한 장점 중 하나로 속성 주입을 사용하면 속성값을 프로그램 변경 없이 외부에서 입력할 수 있게 된다. 이러한 속성 주입 기능을 사용하기 위해서 HttpClientFactoryBean 클래스에서 오브젝트 생성에 필요한 초기 입력 정보를 setter 속성으로 정의했다. 이렇게 setter 속성을 정의하면 Spring Framework는 Bean 정의 시 해당 setter 속성의 property에 값을 주입할 수 있게 된다. 여기에서 setter 속성이란 Bean 형식의 클래스에서 set 으로 시작하는 메서드를 말한다. 이 부분에 대한 자세한 내용은 이 글의 주제를 벗어나므로 이해가 되지 않는 독자는 Spring Framework의 Bean 정의 설명 문서를 참조하면 될 것이다.
Spring Framework의 속성값 주입 기능을 통해 오브젝트 생성에 필요한 초기화 입력 정보를
HttpClientFactoryNoConfigurerBean 클래스 소스의 수정 없이도 수정할 수 있는 장점도 함께 생기게 되었다.
그러나 만약 어떤 애플리케이션 개발 구조상 위 두 속성 정보인 host와 port 정보를 데이터베이스로부터 입력 받는다면 어떻게 될까? 이 경우 속성값을 데이터베이스에 값으로 바로 추출하는 기능은 Spring Framework가 제공하지 않으므로 처음 임의의 Java 오브젝트를 Spring 프레임워크에서 사용하기 위해 부딪쳤던 문제와 유사한 문제에 다시 부딪치게 된다. 즉 Spring Framework의 속성값 주입 기능만으로는 부족하고 다른 방안을 찾아 소스를 수정해야만 된다. Spring Framework의 속성 값 주입 기능을 버리고 HtttpClient 오브젝트를 생성하기 위해 새로운 Spring FactoryBean 클래스 작성하여 host와 port 정보를 데이터베이스에서 읽도록 재 작성해야 한다. 그러나 Factory 로직을 분석하고 해당 초기화 입력 추출 로직을 대체하는 작업을 하는 개발자는 Factory 로직 내의 오브젝트 생성 로직과 초기화 입력 정보 획득 로직이 결합된 로직을 모두 분석해야 할 것이다. 여기서 예를 든 소스는 이 과정이 그렇게 복잡하지 않지만 일반적인 경우 항상 이런 경우를 기대할 수 없을 것이고 생성 과정이 복잡한 경우도 대해서도 고려해야 할 것이다. 이 과정이 복잡한 소스를 개발자가 분석 수정하는 경우 소스에 대한 분석과 수정에 따른 오류 영향도가 커지게 되어 개발 생산성은 저하될 것이다. 그러므로 오브젝트 생성 로직과 초기화 입력 정보 획득 및 등록 로직을 분리하여 할 수 있다면 그리고 개발자는 오브젝트 생성을 위해 단지 초기화 입력 정보 추출과 등록 로직만 수정할 수 있다면 개발 생산성은 높아 지게 될 것이다. 그리고 구조적으로 보면 Factory 개발 측면에서는 오브젝트 생성과 관련된 로직의 은익성을 확보하고 사용 오브젝트 사용 측면에서는 초기화 입력의 유연성을 제공받는다.
그럼 지금까지 Factory 패턴만 적용된 소스와 설정에 설정자 패턴을 추가해 보자. 먼저 Configurer 인터페이스를 정의한다.
Configurer.java
package com.brm.pattern.configurer;
public interface Configurer<T> {
public void configure(T client) throws Exception;
}
Configurer 인터페이스는 초기 입력 정보를 주입할 수 있는 오브젝트를 입력 파라미터로 가지는 configure Method를 정의한다. 이 메서드에서 추출된 초기 입력 정보를 오브젝트에 등록하는 기능을 한다
다음으로 Configurer 인터페이스 구현 클래스를 작성한다.
HttpClientConfiguer.java
package com.brm.pattern.configurer;
import org.ektorp.http.StdHttpClient.Builder;
public class HttpClientConfiguer implements Configurer<Builder> {
private String host;
private int port;
public void configure(Builder builder) throws Exception {
builder.host(host).port(port);
}
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
}
HttpClientConfigurer는 설정자 패턴의 구조를 설명하기 위한 클래스이므로 복잡한 추출 로직을 제시하지 않고 HttpClientFactoryNoConfigurerBean Factory내에서 초기 입력 정보 획득 부분만을 옮겨 왔다. Host와 port를 속성(setter)으로 사용하고 configure Method에서 Builder 오브젝트에 host와 port값을 등록한다. 만약 추출 경로가 데이터베이스라면 HttpClientConfigurer 를 상속받아 새로운 클래스를 (예를 들어 DBConfigurer) 만들고 데이터베이스 추출 로직을 추가하여 데이터베이스에서 추출한 host와 port 속성 값을 등록하는 로직을 추가하면 된다. 여기에서는 HttpClientConfigurer 로만 설명을 진행한다.
이제 HttpClient Factory 에서 초기 입력 정보 추출 로직과 등록하는 로직을 제거하고 Configurer 오브젝트를 호출하는 부분을 추가하면 아래와 같이 좀더 간결한 Factory 클래스가 된다.
HttpClientFactoryBean.java
package com.brm.pattern.configurer;
import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.ektorp.http.StdHttpClient.Builder;
import org.springframework.beans.factory.FactoryBean;
public class HttpClientFactoryBean implements FactoryBean<HttpClient> {
private Configurer<Builder> configurer;
public HttpClient getObject() throws Exception {
Builder builder = new StdHttpClient.Builder();
configurer.configure(builder);
return builder.build();
}
public Class<? extends HttpClient> getObjectType() {
return StdHttpClient.class;
}
public boolean isSingleton() {
return true;
}
public void setConfigurer(Configurer<Builder> configurer) {
this.configurer = configurer;
}
}
보는 바와 같이 host와 port 속성에 대한 로직이 제거되었고 대신 Configurer 주입 속성이 생겼으며 getObject Method에 host와 port 값 등록 로직 대신 configurer 오브젝트의 configure Method 호출 로직으로 대체 되었다. 변경된 Spring XML 파일은 다음과 같다.
application.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="myHttpClientOrg" class="com.brm.pattern.configurer.HttpClientFactoryNoConfigurerBean">
<property name="host" value="localhost" />
<property name="port" value="8080" />
</bean>
<bean id="myHttpClient" class="com.brm.pattern.configurer.HttpClientFactoryBean">
<property name="configurer" ref="configuerer" />
</bean>
<bean name="configurer" class="com.brm.pattern.configurer.HttpClientConfiguer">
<property name="host" value="localhost" />
<property name="port" value="8080" />
</bean>
</beans>
위에서 보면 새로운 Bean Configurer 정의가 추가 되었고 초기 입력 정보와 관련된 속성관리는 Configurer Bean 정의로 옮겨졌다.
이렇게 구조를 변경하여 오브젝트 생성 로직은 초기 입력 정보 추출 경로가 변경되더라도 Factory의 수정은 발생하지 않고 Configurer 구현 클래스만 상속 변경 또는 변경하여 추출 경로 변경에 대응할 수 있는 유연한 구조가 되었다. 이 예에서는 설명을 위해 간단한 구조만 언급하였지만 실제 오브젝트 생성에 필요한 초기 입력 정보는 생성 오브젝트가 복잡하고 다양한 환경에서 운영되기 위하여 적게는 하나의 입력에서 많게는 수 십개 이상의 초기 입력 정보를 가질 수 있다. 이런 경우 설정자 패턴이 더 빛나는 구조가 될 수 있을 것이다.
맺음말
필자가 설정자(Configurer) 패턴이라고 이름을 붙인 이 패턴은 Apache Camel Framework의 분석 과정에서 얻은 것이다. 한 예로 Camel의 Http Component 라이브러리에 설정자 패턴이 적용되어 있다. Camel Http Component는 설정자 패턴을 통해 Apache HttpClient 오브젝트의 수많은 초기 파라미터 입력을 생성 로직과 분리하여 입력할 수 있는 기능을 제공한다. 그리고 Spring Framework에서도 설정자 패턴과 유사한 로직을 볼 수 있는데, 필자의 분석으로는 설정자 패턴으로 명확한 개념으로 정리가 되지 않고 개발 소스 수준의 적용으로 보인다. 그리고 Camel Framework에서도 설정자 패턴을 단지 전략 패턴으로만 인식하고 있고 Camel Component에 일부는 적용되어 있지만 일관되게 적용되지 않고 있는 점으로 미루어 아직 패턴으로 인식하지 않는 것 같다. 그러나 Configurer를 하나의 패턴으로 인식하고 이런 측면에서 오브젝트 생성 프로그램을 개발 할 때 설정자 패턴을 적용하여 설정 사용에 일관성을 부여하면 개발자들에게 오브젝트 생성에 대한 초기 정보 등록의 생산성과 초기 정보 추출 확장성을 제공해 줄 수 있을 것이라 믿는다.
참고 사이트
1) ektorp :
http://github.com/helun/Ektorp
2) Camel HTTP Component :
http://camel.apache.org/http.html