2013년 6월 21일 금요일

MyBatis 프레임워크의 트랜잭션 제어


 MyBatis 프레임워크는 관계형 데이터베이스의 레코드와 자바 도메인 객체 사이의 매핑을 자동화 해주는 ORM(Object Relation Mapping) 프레임워크로 데이터베이스의 SQL 문을 거의 그대로 사용할 수 있게 해준다. 특히 J2EE의 엔티티 빈(entity bean)이나 하이버네이트(Hibernate) 프레임워크에서는 (견해에 따라) 활용하기 어려운 집계나 조인도 데이터베이스의 SQL을 그대로 사용함으로 쉽게 활용할 수 있게 해준다.

 MyBatis 프레임워크는 오랫동안 사랑받아 왔던 ORM 프레임워크인 iBatis 프레임워크를 동일한 개발자들이 내부 구조를 재설계하여 새롭게 만든 ORM 프레임워크이다. (그래서 내부적으로는 iBatis라는 패키지 이름이 계속 사용되고 있다. 심지어 버전도 이어 받고 있다.) MyBatis는 iBatis의 기본 골격을 거의 그대로 유지하고 있으므로, iBatis에 익숙한 개발자라면 MyBatis로 적응하는 것이 그렇게 어렵지 않을 것이다. 필자도 MyBatis 사용자 가이드를 보면서 간단한 MyBatis 프로그램을 어렵지 않게 작성할 수 있었다. 동작하는 MyBatis 애플리케이션을 작성하는 데 하루 정도 걸린 것 같다.

트랜잭션 관리

 최근 프레임워크들은 선언적 트랜잭션 관리(DTM, declarative transaction management)라든지 컨테이너 관리 트랜잭션(CMT, Container Managed Transaction)들의 지원에 많은 노력을 기울이고 있다. 선언적 트랜잭션 관리는 어노테이션 등을 사용하여 메소드나 클래스 단위의 트랜잭션의 흐름을 정의하는 방법이고, 컨테이너 관리 트랜잭션은 컨테이너 서버의 설정으로 컨테이너 내 객체들의 트랜잭션을 관리하는 방법이다. 그런데 선언적 방법 또는 컨테이너 관리 방법 모두 데이터베이스를 연동하는 애플리케이션 개발자들에게 익숙하지 않다는 점이 문제가 된다. 그 결과 이 두 방법으로는 개발자가 트랜잭션이 잘 처리 되었는지 안되었는지 확신하기가 어려울 수 있다. 왜냐면 선언적 방법에서는 어떻게 선언하느냐에 따라 트랜잭션의 범위나 적용 방법이 달라지게 되는데 이것을 개발자가 쉽게 이해하지 못할 수 있고, 컨테이너 기반의 트랜잭션 관리는 J2EE의 엔티티 빈이나 CMT 기반 세션 빈의 실패로 이미 그 개발자들의 적응에 문제가 있음이 증명되었다고 볼 수 있다. 그래도 여전히 이 두 방법이 추구되는 이유는 세상의 모든 데이터를 객체로 다루고 싶은 사람들의 주장이나 이상의 입김이 계속 업계로 작용하는 것이 아닐지 생각해 보게 된다. 아무리 좋은 기술이더라도 개발자가 쉽게 이해하고 사용하고 확인할 수 없다면 좋은 기술이라고 볼 수 없는데, 뛰어나거나 위대한 그루(?)들이 개발자들을 이끌려는 방향이 너무 이상적인 것 같다. 어쨌든 DTM이든 CMT이든 개발자들에게는 너무 어렵다는 것이 필자가 가진 일관된 의견이다.

 그럼 트랜잭션을 프로그램으로 관리하는 것은 어떤가? 트랜잭션을 프로그램을 관리하는 방법은 수십 년 동안 개발자들이 사용해 오던 방법이다. 이 방법을 사용하면, 데이터베이스 처리 로직이 중첩 또는 내포 형식의 복잡한 구조를 갖지 않는 일상적인 구조인 경우, 개발자들은 누구든지 쉽게 트랜잭션의 범위를 명시적으로 프로그램으로 지정할 수 있고, 이렇게 명시적으로 지정된 트랜잭션의 범위는 일반적으로 쉽게 파악된다. 그리고 데이터베이스와 관련된 비즈니스 로직들은 단순한 구조를 갖는 경우가 대부분이다. 더욱이 긴 절차가 필요한 비즈니스에서 일정 절차까지의 진행 상황을 커밋해야 하는 경우엔, 프로그램으로 관리하는 데이터베이스 트랜잭션 방법이 더 적합하다. 선언적 방법으로 진행해야 하는 경우 각 메소드마다 트랜잭션의 전파를 고려해야 하는 어려움이 따르고, 컨테이너 관리 방법도 긴 절차의 트랜잭션을 관리하기 위해서는 더욱 복잡하게 각 단계마다 세션 빈과 같은 것들을 만들어야 한다. 그렇기 때문에 최신 프레임워크들이 DTM이나 CMT을 제시함에도 불구하고 개발자들에게는 프로그램으로 관리하는 데이터베이스 트랜잭션이 여전히 선호되고 있다. 그리고 iBatis 프레임워크는 이런 기본적인 기능에 충실한 프레임워크 중 하나였다.

iBatis의 장점

 iBatis 프레임워크는 데이터베이스 트랜잭션에 있어서 질의 메소드 단위의 자동 커밋과 필요한 경우 언제든지 프로그램으로 트랜잭션의 범위를 지정하게 해주는 유연성을 동시에 제공한다. 그리고 데이터베이스 처리와 관련된 자원도 자동으로 해제한다. 다시 말해 iBatis 프레임워크를 사용면 JDBC API를 다루는 경우 발생하는 connection 객체나 statement 객체들을 해제하기 위해 호출해야 하는 close() 메소드들을 호출하지 않아도 된다. 자원 해제 문제는 애플리케이션의 안정성과 가용성에 아주 중요한 문제를 야기할 수 있다. 제대로 해제 되지 않는 자원 객체들은 자원의 고갈이나 메모리 부족의 문제를 야기시키고 결과적으로 애플리케이션의 다운 문제로 확대될 수 있기 때문이다. iBatis를 사용하면 데이터베이스 연동에서 발생하는 이런 자원 샘 현상도 방지할 수 있다.

 iBatis 프레임워크를 사용하면 데이터베이스를 연동하는 코드를 다음과 같이 간단하게 작성할 수 있다.
…
try {
      sqlMapClient.startTransaction();
      sqlMapClient.update("insertAccountViaParameterMap", account);
      sqlMapClient.commitTransaction();
    } finally {
      sqlMapClient.endTransaction();
    }
…
 위 코드처럼 iBatis를 사용하면 자원 해제를 위해 close() 메소드들을 직접 호출하지 않아도 되며, 필요한 곳에서 언제든지 데이터베이스 트랜잭션을 시작하고 종료할 수 있다. 단 finally 블록 안에 트랜잭션을 종료하는 로직을 반드시 추가하여 트랜잭션이 닫히지 않는 것을 방지해야 한다. 이 코드에서 볼 수 있듯이 iBatis는 데이터베이스 연동에 필요한 코드를 상당히 줄여 줄 뿐만 아니라, 데이터베이스 트랜잭션도 자유롭게 지정할 수 있게 하고, 소스와 SQL 쿼리를 분리함으로 이기종 데이터베이스의 이식성도 좋게 한다. 결과적으로 데이터베이스 비즈니스 개발의 생산성을 향상시킨다. 그리고 이런 장점들이 MyBatis로 이어져 발전하고 있다. 또 위 코드에서 트랜잭션 관련 코드들을 제거하더라도 iBatis가 메소드 단위의 커밋을 지원하게 설정된 경우 메소드가 성공적으로 호출되면 자동으로 커밋까지 실행된다. 그러므로 이 경우 데이터베이스 처리 로직이 단 하나의 갱신이나 추가 메소드를 호출하는 경우 트랜잭션 과련 로직을 굳이 추가하지 않아도 된다. 즉 테이블에 입력할 정보가 준비된 경우, 아래와 같이 단 한 줄로 데이터베이스에 레코드를 추가할 수 있게 된다.
…
sqlMapClient.update("insertAccountViaParameterMap", account);
…

MyBatis의 변화

 그런데 MyBatis에 와서 이상한 변화가 생겼다. 첫째, 그동안 스레드 안전한 SqlMapClient 클래스가 사라졌다. SqlMapClient 객체 대신 스레드 안전하지 않는 SqlSesssion 객체를 사용하여 질의를 수행한다. (SqlSesssion 객체가 스레드 안전하지 않은 이유는 요청(request) 또는 메소드(method) 범위의 객체이기 때문이다.) 또 SqlSession 객체는 자원 해제를 위해 반드시 close() 메소드를 호출해야 한다. 나아진 점도 있다. 데이터베이스의 트랜잭션을 시작하는 메소드가 없어도 트랜잭션을 처리할 수 있다. (즉 openSession 메소드의 호출과 더불어 필요에 따라 자동으로 트랜잭션이 시작된다.) 그 결과 MyBatis API를 사용한 데이터베이스를 연동하는 코드가 다음과 같이 작성된다.
...
SqlSession session = sqlSessionFactory.openSession();
try {
// following 3 lines pseudocod for "doing some work"
    session.insert(…);
    session.update(…);
    session.delete(…);
    session.commit();
} finally {
    session.close();
}
...

 위 코드에서 볼 수 있듯이 MyBatis 프레임워크를 사용하는 경우 사용을 마친 SqlSession 객체는 반드시 닫아 주어야 한다. 즉 finally 블록 등에서 반드시 session.close()를 호출해야 한다. iBatis에서는 없던 호출을 MyBatis에서는 넣어 주어야 한다.  의아한 것은 왜 자원 해제의 누락으로 애플리케이션의 안정성을 해칠 수도 있는 로직을 개발자에게 떠 넘겼냐 하는 것이다. 또 close()를 호출하지 않는 방법을 전혀 제공하지 않는 것도 아니다. MyBatis 프레임워크를 Spring 프레임워크와 결합하여 SqlSessionTemplate 클래스 객체를 사용하면 자원 해제에 대한 문제가 사라진다.

 MyBatis 프레임워크는 Spring 프레임워크에서 MyBatis를 통합하여 사용할 수 있게 MyBatis-Spring를 제공한다. MyBatis-Spring를 통해 만들어진 SqlSessionTemplate 객체는 SqlSession 객체와 달리 내부적으로 인터셉터를 통해 자동으로 close()를 호출하여 자원 해제 문제를 해결한다. 개발자는 SqlSessionTemplate 객체를 사용하는 경우 더 이상 MyBatis의 자원 해제 문제를 신경 쓰지 않아도 된다. 그런데 대신 MyBatis-Spring은 다른 문제를 야기한다. 데이터베이스 트랜잭션의 문제이다. MyBatis-Spring을 사용하는 경우, MyBatis의 SqlSessionTemplate 객체는 commit(), rollback() 메소드를 사용할 수 없다. 즉 SqlSessionTemplate 객체를 이용하여 프로그램적으로는 트랜잭션을 관리할 수 없게 한다. 억지로 SqlSessionTemplate 객체의 commit() 메소드를 호출하면 다음과 같은 예외를 발생한다.
java.lang.UnsupportedOperationException: Manual commit is not allowed over a Spring managed SqlSession
 at org.mybatis.spring.SqlSessionTemplate.commit(SqlSessionTemplate.java:278)
 at com.brm.mybatis.MybatisSupportTest.testProgramacTraction(MybatisSupportTest.java:28)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:601)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:240)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:180)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


Spring 프레임워크의 트랜잭션 관리

 SqlSessionTemplate 객체를 이용하여 MyBatis의 자원 해제 문제를 해결하고 나니, 프로그램 방법의 데이터베이스 트랜잭션 관리가 불가능해지는 새로운 문제가 등장했다. 이 문제에 대해 MyBatis 문서는 Spring 프레임워크의 프로그램적 트랜잭션 관리 방법을 사용할 수 있다고 언급한다. 한 가지는 TransactionTemplate을 이용한 프로그램 방법이고 다른 가지는 PlatformTransactionManager를 사용한 프로그램 방법이 있다고 언급한다. 프로그램으로 데이터베이스 트랜잭션을 관리하려고 했더니, Spring 프레임워크를 이용하라고 한다. 이 부분이 참 의아한 부분이다. 이전에 iBatis에서 제공하는 SqlMapClient 객체는 이런 번거로운 작업 없이도 자원 해제와 트랜잭션을 모두 자체적으로 처리를 해 주었는데, 왜 MyBatis에서는 이렇게 바뀌었는지 굳이 Spring 프레임워크에 의존해야 했는지, 그런 경우라도 기본 구현을 내부에 포함하면 될 일인데, 굳이 MyBatis를 사용하는 개발자들에게 Spring 프레임워크를 사용하여 직접 구현하게 할 필요가 있었는지, 필요한 다른 이유가 있었던 것인지, MyBatis 커미터들이 게으른 것인지?. 어쨌든 현실은 적응해야 한다.

 필자는 Spring 프레임워크의 데이터베이스 관련 Template 시리즈 클래스 객체들을 아주 싫어한다. 그 이유는 콜백처리 때문이다. 질의 하나를 호출하려고 해도 콜백 방식을 사용해야 한다. (필자가 데이터베이스 처리에 있어서 Spring 프레임워크보다 iBatis 프레임워크를 더 선호했던 이유가 바로 iBatis에서는 일반적인 질의에 콜백을 사용하지 않기 때문이기도 했다.) Spring의 TransactionTemplate도 예외는 아니다. TransactionTemplate를 사용하려면 반드시 콜백 방식을 사용해야 한다. 다음은 TransactionTemplate를 사용하는 Spring 문서의 코드에 TransactionTemplate 객체를 사용하는 코드 부분을 추가한 예이다.
public class SimpleService implements Service {
    
    private final TransactionTemplate transactionTemplate;
    
    public SimpleService(PlatformTransactionManager transactionManager) {
        Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        
        // the transaction settings can be set here explicitly if so desired
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 seconds
    }
    ...

    public int insertUser(final User user) {
        //use TransactionCallback handler if some result is returned
        return    transactionTemplate.execute(new TransactionCallback<Integer>() {
            public Integer doInTransaction(TransactionStatus paramTransactionStatus) {
                String inserQuery = "insert into users (username, password, enabled , id) values (?, ?, ?, ?) ";
                Object[] params = new Object[]{user.getUserName(), user.getPassword(),user.isEnabled(),user.getId()};
                int[] types = new int[]{Types.VARCHAR,Types.VARCHAR,Types.BIT,Types.INTEGER};
                return jdbcTemplate.update(inserQuery,params,types);
            }
        });
    }
}
 프로그램적으로 데이터베이스 트랜잭션을 처리하기 위해 이렇게 복잡하게 일을 해야 하다니, MyBatis 프레임워크는 프로그램 방법의 데이터베이스 트랜잭션을 Spring에 떠 넘기고 Spring 프레임워크는 여전히 자신들의 콜백 방식을 고집한다.

 콜백이 없는 두 번째 방법으로 PlatformTransactionManager를 사용하는 방법이 있다. 다음은 PlatformTransactionManager를 사용하는 Spring 문서의 코드 예이다.
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can only be done programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
  // execute your business logic here
}
catch (MyException ex) {
  txManager.rollback(status);
  throw ex;
}
txManager.commit(status);
 위 코드도 여전히 복잡하다. iBatis에서는 프로그램에서 데이터베이스 트랜잭션을 사용할 때 startTransaction(), commitTransaction(), rollbackTransaction(), endTransaction() 메소드들만 호출하면 됐었는데. 위 코드에서 보다시피 데이터베이스 트랜잭션이 필요한 경우 몇 개의 객체들을 추가로 생성하고 관련 메소드들을 호출해야 한다.

문제 해결

 어떻게 하면 MyBatis도 자원 해제에 대해 자유로워 지고, 편리하게 프로그램으로 데이터 트랜잭션을 관리할 수 있을까? 구글링을 해보고 관련 문서들을 다 찾아 보았지만 원론적인 설명 밖에는 찾을 수 없었다. 그래서 직접 이 문제를 해결하기로 했다.

 필자도 MyBatis 프레임워크와 Spring 프레임워크를 결합하여 사용하는 것을 권장한다. 이 경우 일차적으로 자원 해제 문제가 해결된다. 남은 문제는 프로그램으로 관리할 수 있는 편리한 데이터베이스 트랜잭션 방법을 찾아 내는 것이다. 필자는 소스 코드의 흐름의 방해하는 콜백 방식을 사용하지 않는 PlatformTransactionManager을 사용하여 이 문제를 해결했다.

 문제 해결 전략은 DefaultTransactionDefinition, PlatformTransactionManager, TransactionStatus 클래스를 묶어 내부적으로 처리하는 새로운 클래스를 정의하고, iBatis의 SqlMapClient 클래스 객체에서 호출하는 트랜잭션 절차 메소드들을 이 새로운 클래스에서 노출시키고, 이 과정에서 Spring 프레임워크의 빈 자동 주입, 프로토타입 빈 등을 사용하고, 위임(delegation)과 상속(inheritance)를 적절히 이용하여 추가되는 코드 량을 최소화 하는 것이었다. 이런 전략을 바탕으로 MyBatisTransactionManager 클래스와 지원 클래스인 MyBatisSupport 클래스를 다음과 개발했다.

MyBatisTransactionManager.java
package com.brm.mybatis.transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Service
@Scope("prototype")
public class MyBatisTransactionManager extends DefaultTransactionDefinition {

 private static final long serialVersionUID = -1375151959664915520L;

 @Autowired
 PlatformTransactionManager transactionManager;

 TransactionStatus status;

 public void start() throws TransactionException {
  status = transactionManager.getTransaction(this);
 }

 public void commit() throws TransactionException {
  if (!status.isCompleted()) {
   transactionManager.commit(status);
  }
 }

 public void rollback() throws TransactionException {
  if (!status.isCompleted()) {
   transactionManager.rollback(status);
  }
 }

 public void end() throws TransactionException {
  rollback();
 }
}
 위 코드에서 MyBatisTransactionManager 클래스는 DefaultTransactionDefinition를 상속하여 DefaultTransactionDefinition를 위임할 경우 발생하는 수많은 위임 메소드들을 정의하지 않게 했고, MyBatisTransactionManager가 프로토타입 빈으로 정의되는 것을 감안하여 자유롭게 멤버 변수를 선언했고, transactionManager 멤버 변수는 Spring의 어노테이션 주입 기능을 이용하여 불필요한 설정자(setter) 메소드를 정의하지 않았다.

MyBatisSupport.java
package com.brm.mybatis;

import com.brm.mybatis.transaction.MyBatisTransactionManager;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

@Service("myBatisSupport")
public class MyBatisSupport {

 @Autowired(required = false)
 @Qualifier("sqlSession")
 protected SqlSessionTemplate sqlSession;

 @Autowired
 ApplicationContext applicationContext;

 public MyBatisTransactionManager getTransactionManager() {
  return applicationContext.getBean(MyBatisTransactionManager.class);
 }
}
 MyBatisSupport 클래스는 간단하지만 MyBatis를 이용하려는 애플리케이션 클래스들이 간단한 상속이나 주입만으로 언제든지 데이터베이스를 연동할 수 있는 SqlSessionTemplate 객체와 데이터베이스 트랜잭션을 프로그램적으로 지원하는 MyBatisTransactionManager 객체를 이용할 수 있게 해준다. 단 MyBatisSupport 클래스가 Spring 프레임워크의 어노테이션과 컨텍스트를 사용함으로 MyBatisSupport를 상속하는 애플리케이션 클래스들도 Spring 프레임워크 내에서 동작하게 해야 한다. 애플리케이션 클래스들을 Spring 프레임워크 내에서 동작하게 하는 방법은 Spring 프레임워크의 문서를 참조하기 바란다. 필자는 프로그램적 데이터베이스 트랜잭션을 데트스하는 JUnit MybatisSupportTest 클래스에 Spring-Test의 @RunWith와 @ContextConfiguration 어노테이션을 사용하여 Spring 프레임워크가 MybatisSupportTest 클래스의 Spring 어노테이션들을 자동으로 조사하여 객체를 주입하게 했다.

 다음은 MyBatisTransactionManager를 테스트하는 MybatisSupportTest 클래스 소스이다. MybatisSupportTest 클래스는 MyBatisSupport 클래스를 상속한다.

MybatisSupportTest.java
package com.brm.mybatis;

import com.brm.mybatis.transaction.MyBatisTransactionManager;

import java.sql.SQLException;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/mybatis.xml" })
public class MybatisSupportTest extends MyBatisSupport {

 @Test
 public void testProgramacTraction() throws SQLException {

  MyBatisTransactionManager transaction = getTransactionManager();
  try {
   
   transaction.start();

   List results = sqlSession.selectList("test.select");
   System.out.println("selected = " + results);
   int cnt = sqlSession.update("test.insert", results.get(0));
   System.out.println("inserted = " + cnt);

   transaction.commit();

  } finally {
   transaction.end();
  }
 }
}

 위 코드를 보면 SqlSessionTemplate 객체를 주입하는 부분도, 자원 해제를 위한 close() 메소드의 호출도 없다. 스레드에 안전한 SqlSessionTemplate 객체는 MyBatisSupport 지원 클래스에서 자동으로 주입되고, 자원 해제는 MyBatis-Spring 내부에서 인터셉터를 통해 자동으로 처리된다. 트랜잭션 매니저 객체인 MyBatisTransactionManager 객체는 MyBatisSupport 클래스의 getTransactionManager() 메소드 호출을 통해 Spring 프레임워크의 애플리케이션 컨테이너로부터 반환 받는다. MyBatisTransactionManager 객체는 iBatis SqlMapClient 객체의 데이터베이스 트랜잭션 처리 메소드들과 거의 유사한 메소드들을 애플리케이션에게 노출한다. 차이점은 iBatis SqlMapClient 객체는 내부적으로 트랜잭션 관련 인스턴스들을 생성하고 관리하는 반면 MyBatisTransactionManager 객체는 getTransactionManager() 메소드를 통해 획득된다는 점이다.

 다음은 MyBatis 프레임워크를 Spring 프레임워크와 함께 사용하기 위한 Spring 프레임워크의 빈 정의 XML인 mybatis.xml이다. 이곳에 MyBatisTransactionManager 클래스가 빈으로 등록된다. (어노테이션 스캔을 활용하는 경우, 이런 정의조차 없앨 수 있다.) 나머지 부분은 데이터 소스와 트랜잭션 관리자 SqlSessionTemplate과 관련된 빈들의 등록이다.

mybatis.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:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
 xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="
 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

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

 <!-- Data Source -->
 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="${datasource.driver}" />
  <property name="url" value="${datasource.url}" />
  <property name="username" value="${datasource.username}" />
  <property name="password" value="${datasource.password}" />

 </bean>

 <!-- Transaction Manager -->
 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
 </bean>

 <bean class="com.brm.mybatis.transaction.MyBatisTransactionManager" scope="prototype" />

 <!-- MyBatis Sql Session Template -->
 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations">
   <list>
    <value>classpath*:${mybatis.query.locations}</value>
   </list>
  </property>
 </bean>

 <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
 </bean>
</beans>
 위 XML에서 MyBatisTransactionManager 빈은 스코프가 prototype이다. 그러므로 애플리케이션이 데이터베이스의 트랜잭션을 위해 MyBatisSupport 클래스의 getTransactionManager 메소드를 호출하면 Spring 프레임워크는 새로운 MyBatisTransactionManager 객체를 생성하여 반환한다.

 mybatis.xml에서 참조하는 설정 속성 파일인 app.properties는 다음과 같다. 테스트 데이터베이스는 오라클 데이터베이스를 사용했다.

app.properties
datasource.driver = oracle.jdbc.driver.OracleDriver
datasource.url = jdbc:oracle:thin:@192.168.1.50:1521:ORACLE
datasource.username = ******
datasource.password = ******
mybatis.query.locations =  com/brm/**/sql/*.xml

 MyBatis 프레임워크를 사용하여 데이터베이스 연동 로직을 개발 할 때, 위에 설명한 MyBatisTransactionManager 클래스와 MyBatisSupport 클래스를 추가하면, iBatis를 사용할 때와 유사한 방법으로 자원 해제와 데이터베이스 트랜잭션을 처리할 수 있게 된다. 또한 위 테스트 코드에서 MyBatisTransactionManager의 데이터베이스 트랜잭션 호출들을 제거하는 경우, MyBatis의 SqlSessionTemplate 객체는 내부에서 인터셉터를 통해 세션을 닫을 때 자동으로 커밋을 호출해 준다. 즉 데이터베이스 트랜잭션 처리 로직을 제거하면, SqlSessionTemplate의 메소스 호출 단위로 처리 성공 시 자동 커밋이 진행된다. 그러므로 데이터베이스 처리를 트랜잭션으로 묶을 경우 MyBatisTransactionManager 객체를 사용하고, 메소드 단위로 트랜잭션을 사용하는 경우 SqlSessionTemplate의 메소드를 단독으로 호출하면 된다.

 다음은 MyBatis를 테스트하기 위해 사용한 Maven의 pom.xml 파일이다. 사용된 jar 들을 아래 pom.xml을 통해 다운받을 수 있다.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>com.brm</groupId>
 <artifactId>mybatis-practice</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>mybatis-practice</name>
 <url>https://github.com/hinunbi/mybatis-practice</url>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>
 <repositories>
  <repository>
   <id>mesir-repo</id>
   <url>http://mesir.googlecode.com/svn/trunk/mavenrepo</url>
  </repository>
 </repositories>
 <dependencies>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-beans</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>3.0.5.RELEASE</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>commons-dbcp</groupId>
   <artifactId>commons-dbcp</artifactId>
   <version>1.4</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.2</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-spring</artifactId>
   <version>1.2.0</version>
  </dependency>
  <dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis</artifactId>
   <version>3.2.2</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>com.oracle</groupId>
   <artifactId>ojdbc14</artifactId>
   <version>10.2.0.4.0</version>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.6.1</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-log4j12</artifactId>
   <version>1.6.1</version>
   <type>jar</type>
   <scope>compile</scope>
  </dependency>
 </dependencies>
</project>
 지금까지 MyBatis와 Spring 프레임워크를 이용하여 iBatis 만큼 편리하게 MyBatis를 사용하는 방법을 설명했다. 이 블로그에서 설명한 클래스들 잘 활용하면 불필요한 반복 코드들을 더욱 줄일 수 있고 데이터베이스의 트랜잭션 처리도 좀더 직관적이고 명시적으로 표현할 수 있게 될 것이다. 이 글의 소스는 GitHub에 올려 놓았다. 아래 참고 사이트의 "프로그램 소스" 링크에서 내려받을 수 있다.

맺음말

 MyBatis는 iBatis의 대를 이은 훌륭한 프레임워크이다. MyBatis는 iBatis에서는 지원하지 않았던 내포 관계를 가진 여러 테이블의 레코드들을 복합 구조의 자바 도메인 객체로 매핑하게 해주는 기능을 제공하는 등, 그동안 iBatis에서 아쉬웠던 기능들을 추가적으로 제공해 주고 있다. 그리고 MyBatis 프로젝트는 계속 진행 중임으로 개발이 중단(?)된 iBatis처럼 어느 순간에 최신의 데이터베이스와 맞지 않게 될 위험성도 없다. 그러므로 이 글에서 설명한 자원 해제와 트랜잭션의 해결 방법을 적용한다면, iBatis만큼 편리하면서도 기능적으로도 더욱 풍부한 생산적인 프레임워크로 MyBatis를 활용할 수 있을 것이다.

참고 사이트)


2013년 6월 3일 월요일

교착 상태(Deadlock)

 대규모의 동시 처리 시스템을 개발하는 경우, 전통적인 프로그램 모델은 멀티 프로세스나 스레드를 사용하는 것이다. 멀티 프로세스나 스레드는 공유 자원(메모리, 파일, 디바이스 등)을 사용하여 상태나 정보를 공유한다. 즉 정보를 읽거나 쓴다. 멀티 프로세스나 스레드들은 경우에 따라 공유 자원을 동시에 사용하므로 동시성 문제가 발생한다. 이를 방지하기 위해 프로세스나 스레드들은 상호 배타적인 동기 메커니즘을 사용한다. 즉 프로그램에 잠금(lock)을 사용하여 멀티 프로세스나 스레드들이 공유 자원을 동시적에 사용하는 것을 배타적으로 관리한다. 크리티컬 섹션(critical section), 뮤텍스(mutex), 세마포(semaphore) 같은 것들이 이런 잠금이다. 그런데 멀티 프로세스나 스레드들이 공유 자원을 배타적으로 이용하다 보면 문제가 발생한다. 일반적으로 잠금을 사용하면 공유 자원을 사용하는 데 있어서 올바른 행동을 보장받을 수 있지만, 그럼에도 불구하고 공유 자원을 사용하는 프로세스나 스레드들은 어쩌다가 교착 상태(deadlock)에 빠지게 된다. 다음 그림은 이런 교착 상태의 예이다.




 위 그림처럼 두 스레드가 두 자원을 잠금을 사용하면서 동시에 배타적으로 참조하는 시스템인 경우, 두 스레드는 이상 없이 잘 동작하다가 어느 순간에 아무 것도 실행하지 않고 공유 자원의 잠금이 풀리기만을 기다리는 교착 상태에 빠지게 된다. 그런데 이런 교착 상태가 실행 중 발생하는 것을 예상하는 것은 상당히 어렵다. 왜냐면 일반적인 교착은 위 그림처럼 단순한 것이 아니라, N개의 자원과 스레드들이 참여하여 발생하기 때문이다. N의 자원과 스레드들이 포함된 순환 교착을 예측하거나 해결하는 것은 고도의 프로그램 기술을 가진 개발자나 아키텍트에게도 결코 쉬운 일이 아니다. 일단 이런 문제는 디버깅하는 것도 상당히 어렵다. 일정 규모의 시스템을 개발하거나 운영해본 전문가들은 이런 문제들이 실제로 가끔 발생하며, 원인도 알 수 없고, 유일한 해결책(?)은 시스템의 재기동이란 것을 경험해 보았을 것이다. 이런 불가사의한 문제의 원인 중 하나가 바로 일정 개수 이상의 공유 자원과 스레드들이 포함된 순환 교착(circular deadlock)이다.

 좀더 구체적인 예를 들어보자. 프로그램에서 싱글톤 패턴(Singleton pattern)을 사용하는 경우 교착 상태에 빠질 수 있다. 싱글톤이 바로 공유 자원이며 이 싱글톤에 값을 쓰거나 읽기 위해 잠금을 사용하는 스레드들이 경쟁하기 때문이다. 싱글톤은 상당히 유용한 패턴이고 많은 곳에서 활용된다. 그러므로 개발자가 프로그램에서 사용하지 않더라도 개발자가 쓰는 프레임워크나 라이브러리 내부에서도 사용될 수 있다. 물론 프레임워크나 라이브러리는 정교하게 동기화되기 때문에 거의 교착에 빠지지 않을 것이지만, 그럼에도 불구하고 싱글톤이 개발자도 모르게 사용될 수 있고, 우연치 않게 여러 라이브러리들이 조합되면서 여러 싱글톤이 여러 스레드들과 경쟁하는 상황이 만들어지면서 교착은 발생할 수 있다.

 이번엔 좀더 현실적인 예를 보자. 일반적으로 웹에서 서블릿 프로그램을 개발할 때, 서블릿 클래스에 멤버 변수를 정의하지 말라는 말을 한다. 왜냐면 서블릿 객체는 톰캣이나 웹로직 서버 등과 같은 서블릿 컨테이너에서 싱글톤처럼 사용되기 때문이다. 다시 말해 서블릿 컨테이너는 한 번 생성한 서블릿 객체를 재활용 한다. 그런데 이런 사실은 모르는 개발자들이 의외로 많다. 그 결과 일반 클래스처럼 멤버 변수를 선언하고 웹 요청을 처리하는 과정에서 해당 멤버 변수에 읽고 쓰기를 반복하는 경우, 불규칙적으로 멤버 변수의 자료 손상을 경험하게 된다. 그리고 이 현상은 단위 테스트를 진행할 때에는 전혀 발견되지 않고 있다가, 실제 운영 환경에서 실행될 때 발견되는 경우가 많다. (실제로 모 금융 기관의 시스템에서도 이런 현상이 운영 중에 가끔 발생하여, 결국 운영 중에 해결되었다.) 그 이유는 단위 테스트 과정은 스레드들이 경쟁하지 않는 환경으로 멤버 변수를 훼손하는 원인이 제거되어 있고, 통합 테스트나 부하 테스트, 인수 테스트 과정도 멤버 변수를 훼손할 정도의 스레드 경쟁 상황이 쉽게 발생하지 않다가, 운영 환경에 와서야 비로소 여러 스레드들이 제대로 경쟁함으로 서블릿 클래스의 멤버 변수를 훼손하는 동시 사용의 순간이 등장하기 때문이다.

 또 다른 예로, 데이터베이스에서도 교착은 상당히 빈번히 발생한다. 데이터베이스는 여러 세션들이 공유 자원인 테이블을 공유하면서 빈번히 테이블에 레코드를 쓰거나, 읽거나, 갱신하거나, 삭제한다. 이 과정에서 데이터베이스는 자원의 무결성을 보장하기 위해 동기 메커니즘을 사용한다. 데이터베이스는 묵시적이든 명시적이든 트랜잭션을 사용하는데, 바로 이 트랜잭션이 바로 동기 메커니즘 즉 잠금이다. 그러므로 트랜잭션에 여러 테이블들이 포함되고 여러 세션들이 이 트랜잭션을 동시에 실행하는 경우, 프로그램들은 교착에 빠질 수 있다. 그러므로 대부분의 데이터베이스는 이런 교착 상태를 해결하는 메커니즘을 별도로 내장하는데, 오라클의 경우 교착 상태를 판단하는 데 기본적으로 60초를 기준 시간으로 한다. 오라클 데이터베이스 엔진은 60초 동안 응답이 없으면 교착 상태로 판단하고 관련 세션들 중 일부를 끊어 문제를 해결한다. 그러나 실시간 비즈니스에서 데이터베이스의 교착은 60초 동안 거래가 중단됨을 의미한다. 극단적인 예로 미사일 방어 시스템이 적의 미사일을 60초 동안 추적하지 못했을 경우를 생각해 보라! 또는 금융 거래 시스템에서 이체 마감 시간에 이체가 60초 동안 중단된 경우를 생각해 보라!

 일반적으로 대규모 동시 처리 시스템에서 모든 플랫폼들이 정상적으로 운영 중이고, 모든 미들웨어와 프로그램들이 정상적으로 운영 중임에도 불구하고 교착은 발생할 수 있다. 그리고 이 교착은 시스템 다운은 아니지만 보이지 않는 거래 중지를 만들어 마치 해당 시스템이 다운된 것과 같은 효과를 발휘한다. 일반적으로 기업은 시스템의 가용성 문제를 해결하기 위해 이중화나 HA(High Availability), 클러스터 등 고비용을 투자한다. 그에 비해 애플리케이션 소프트웨어의 성능이나 품질에 대해서는 제대로 대비하거나 관리하지 않고 있는 것이 현실이다. 대비라고 한다면 또다시 고가의 상용 플랫폼이나 미들웨어를 도입하여 문제를 해결하려고 한다!!! 그러나 애플리케이션이 발생시키는 교착의 문제는 애플리케이션이 동작하는 플랫폼이나 미들웨어가 해결해 주는 것이 아니다. 단지 그들은 보조적인 모니터링이나 기술지원을 해줄 수 있을 뿐이다. 즉 애플리케이션에서 해결해야 하는 문제이다. 그러므로 만약 이런 문제를 사전에 대비하는 경우, 교착으로 인한 서비스의 중단을 해결하는 것뿐만 아니라, 경우에 따라서는 최적화된 잠금 관리로 마법 같은 안정성과 성능 향상을 이룰 수 있게 된다.

 기업 입장에서 볼 때, 대규모 동시 처리 시스템 개발에 애플리케이션의 교착 방지 등 가용성을 위한 전문 인력을 추가적으로 활용하는 것이 추가적인 비용처럼 보일지 모르지만, 이들은 소프트웨어의 가용성과 그에 따른 성능을 상대적으로 높여줌으로,  가용성을 위해 필요한 하드웨어나 플랫폼, 미들웨어의 비용을  절감시켜 준다. 즉 개발 및 유지보수에 따른 전체 비용은 줄어 들 수 있다. 그러므로 대규모 동시 처리 시스템을 개발에는 애플리케이션들의 교착을 방지하고 소프트웨어의 가용성을 높이기 위해 멀티 프로세스나 멀티 스레드 애플리케이션 개발 경험 많은 개발자나 아키텍트가 설계와 개발에 참여도록 해야야 한다.



2013년 5월 8일 수요일

Getter, Setter 번역


 필자는 지금 "기업 통합 패턴"을 번역하는 중이다. 처음 하는 번역이라 그런지, 번역이란 것이 생각한 것보다 더 고되고 또 군데 군데 등장하는 장애물들도 헤쳐나기가 쉽지 않다.

 그 중 자바의 빈 클래스(Bean Class)에 대한 부분을 만났고 그 안에서 set이란 동사를 만난다. 이 부분을 번역하면서 그냥 설정한다이렇게 하고 넘어가려다가 문득 Setter? Getter? 번역어가 어떻게 될까 궁금증이 생겼다.

 너무 간단한 단어인지라 그 동안 굳이 한글로 생각하지도 않았던 단어인데, 번역에 사용하려면 정확한 번역어로 옮겨야 하기에 더욱 궁금증이 생겼다.

 먼저 네이버 지식 사전을 검색해 보았다. 그런데 그 결과는 너무나 실망스러웠다. 컴퓨터 분야에 풀이된 번역어가 없었다.

다음은 구글링을 해 보았다. 고작 검색 결과가 이백여 개 정도 검색되었다그 결과를 일부 정리해 보면, 자바에서는 Setter는 세터, 설정자, 수정자, Getter는 게터, 접근자로 사용되었고, C#에서는 클래스의 속성에 접근하는 메소드의 통칭을 접근자(accessor)라 하고, 각각을 set 접근자, get 접근자라고 했다. 수정자란 단어는 토비의 스피링 3”에서도 찾을 수 있었다. 그러나 수정자는 Modifier의 번역어이기도 하다.

 Getter, Setter란 단어의 번역어를 찾는 일은, 이 간단한 단어들은 굳이 번역하지 않아도 전산에 종사하는 대부분의 사람들은 이해할 수 있는데, 너무 고민을 많이 하는 것 아니냐는 소리도 들을 수 있다.

 그러나 정말 그렇게 생각한다면 우리가 영어를 공용어로 사용면 되지 굳이 번역이란 것을 고민하면서 귀찮게 한글이며 국어며 이런 것을 사용할 필요가 있겠는가? 라고 논리도 펼 수 있을 것이다. 좀 극단적일 수도 있겠지만……

 예전에 어떤 번역가가 동양에는 없는 Society란 단어를 음차를 사용하여 사회란 단어로 번역을 했다. 그 결과 우리는 사회라는 단어를 갖게 되었고, 이제는 사회를 Society와 연관 짓지 않아도 사회를 그냥 사회로 이해할 수 있게 되었다. 그리고 더 나아가 사회는 사회로부터 새로운 개념을 도출할 수 있을 정도의 확고한 단어가 되었다.

 이렇게 우리는 우리 글로 되어 있을 때, 그 의미를 가장 잘 이해할 수 있다. 그러므로 우리가 갖지 못한 단어를 번역하는 일은 중요하다. 이런 맥락에서 SetterGetter도 적절한 번역어를 확정해야 할 것이다.

 인터넷 상의 여러 문서를 참조한 결과, 필자가 보기에 Setter설정자 Getter획득자로 번역하는 것이 가장 합당한 것 같다. 이 두 용어가 여러 프로그램 언어에 걸쳐 가장 널리 사용되고 있고 영어 원 뜻과도 가장 잘 맞는 것 같다.

 그런데 이 설정자(Setter)는 필자가 정의한 설정자 패턴의 설정자(Configurer)와 용어와 겹친진다. 그러나 두 용어는 사용 환경에 따라 구분 가능할 것임으로 둘 사이 중복은 받아들일 정도이다.

 혹시 Getter Setter에 대한 번역어를 고민하거나 한글이나 우리나라 말로 하면 어떻게 될까 궁금한 사람이 있다면, 이 번역 의견을 참조하기를 바란다. 필자도 앞으로는 글을 쓰거나 번역을 할 때 GetterSetter를 '획득자'와 '설정자'로 사용할 것이다.


  • 사전 참고 Getter 
  • 자바 접근자, 설정자
  • 스칼라 획득자, 설정자
  • 파이썬 획득자, 설정자
  • 스몰토크 획득자, 지정자
  • C# 접근자, get 접근자, set 접근자

2012년 8월 20일 월요일

동기 호출(Synchronous Call)에 대한 고찰


필자가 올린 “동기 호출(Synchronous Call), 비동기 호출(Asynchorous Call)” 블로그에서는 동기 호출, 비동기 호출의 정의에 대해 설명했었다. 여기에 그 정의를 다시 옮겨 보면 다음과 같다.
애플리케이션 프로그램에서 동기 호출(Synchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업(프로시저, 함수, 메서드) 요청 시 요청된 하부작업이 진행되는 동안 호출 프로세스(스레드)의 실행 흐름이 멈추게 되는 호출을 말한다.
애플리케이션 프로그램에서 비동기 호출(Asynchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업 요청 시 요청된 하부작업의 실행 또는 종료와 관계없이 호출 프로세스(스레드)의 실행 흐름은 계속되는 호출을 말한다. 이런 호출 방식에서는 하부작업의 실행완료 시점을 호출 프로세스(스레드)가 정확이 알지 못하므로 호출 프로세스(스레드)와 하부작업은 하부작업의 실행 결과를 둘 사이 약속된 결과 영역 조회나 하부작업이 호출 프로세스(스레드)를 호출하는 Callback 메커니즘을 통해 확인한다.

지금까지 컴퓨터는 빠른 계산 능력을 사용하여 사용자에게 최대한 빠른 응답을 제공하는 방향으로 발전해 왔다. 그 결과 사용자는 컴퓨터에 어떤 요구를 하던지 거의 실시간으로 컴퓨터로부터 응답을 받게 되었다. 만약 컴퓨터에 요구한 응답이 몇 초 이상 걸리게 되는 경우 사용자는 몇 초의 기다림에 불안감까지 느낄 정도로 컴퓨터는 사용자의 요구에 대한 신속한 처리를 보장해 주었다.

이런 빠른 응답을 제공하기 위하여 컴퓨터 하드웨어는 무어의 법칙에 따라 18개월마다 성능을 두 배씩 성능을 향상해 왔다. 그러나 그 하드웨어에서 동작하는 프로그램은 상당히 정체된 발전 단계를 거쳐 왔다. 한 언어가 주류 언어가 되면 적어도 10년 이상 그 언어로 프로그램은 작성되고 그 뒤로도 몇 십 년을 사용하는 방식이었다. 예를 들어 현재 주류 언어 중 하나인 C (대표적 구조적 프로그램 언어) 언어는 태어난 지 이미 40년이 지난 언어이고 Java (객체 지향 언어) 언어도 1995년에 때어나 벌써 17년이나 지나고 있다. 이렇게 하드웨어 발전 속도와 프로그램 언어 사이에 발전의 차이는 프로그램에서 발전된 하드웨어를 최대한 활용하는 점점 어려운 상황에 접어 들게 되었다.

예를 들어 하드웨어 CPU가 늘어났지만 프로그램은 한 CPU에서 밖에 동작하지 못한다던 지, 하드웨어 레지스터 크기가 확장되어 더 많은 정보를 표현할 수 있지만 프로그램 언어의 자료형이 지원을 하지 못한다던 지 등 하드웨어에 도입된 중요한 기술을 프로그램은 좀더 오랜 기간을 거쳐야 비로소 그 기술에 적응하고 활용할 수 있게 된다. 그 중에서도 작업 처리 측면에서 보면 현재 주류 프로그램 언어들이 가진 순차적 명령 처리를 패러다임은 이런 하드웨어 성능 활용에 최대 걸림돌이 되고 있다. 즉 모든 실행은 순서를 가지고 하나씩 실행해 나가는 방식으로 어떤 일이던지 시작에서 끝까지 실행 흐름이 줄을 서서 실행된다. 이 방식은 객체 지향 언어에서도 마찬가지인데 여기서는 액터와 오브젝트 사이의 역할을 나누지만 실행 흐름에 대해서는 순차적 명령 처리 패러다임의 범주에서 벗어나지 못하고 있다. 즉 하부작업 요청 시 함수(메서드) 호출과 결과 리턴이라는 기본 골격은 구조적 프로그래밍 언어와 같다.

일반적으로 프로그램은 실행 흐름작성, 하부작업 실행, 하부작업 실행 결과 활용 등으로 작성된다. 이 과정에서 하부작업 실행과 그 결과를 구성하는 방식은 일반적으로 동기 호출 방식을 사용하게 된다. 즉 하부작업 요청 시 호출 쪽 본작업은 실행을 멈추고 하부작업이 리턴 될 때까지 대기하는 방식이다.

동기 호출은 하부작업의 결과물 획득에 순차적 처리가 가능한 리턴이라는 직관적이고 간결한 메커니즘을 제공한다. 즉 동기 호출은 하부작업이 진행되는 동안 호출 프로세스(스레드)는 실행이 중지되고 하부작업의 리턴 시 자동으로 실행이 계속되어 실행의 순차적 논리흐름을 잘 표현할 수 있다. 이에 비하여 비동기 호출은 하부작업의 완료까지 프로세스(스레드)가 대기하지 못하므로 실행의 순차적 논리흐름을 표현하는데 어려움이 있다. 좀더 구체적으로 비동기 호출은 하부작업의 결과물을 획득하는 메커니즘으로 Call Back 방식을 통한 결과 획득 또는 약속된 장소에 하부작업 결과를 확인하는 Poll 방식이 있는데 Call Back 방식은 호출 프로세스(스레드)의 작업에 대하여 순차적 처리 흐름을 보장하지 못하고 Poll 방식은 호출 프로세스(스레드)의 순차적 작업 흐름 처리는 가능하지만 동기 방식에 비하여 특별한 장점도 없으면서 처리 과정만 복잡한 문제를 일으킨다. 결론적으로 동기 호출은 실행의 순차 처리가 쉽고 비동기 호출은 실행의 순차 처리가 어렵다. 이런 측면에서 애플리케이션 개발 시 동기 호출은 비동기 호출에 비해 훨씬 지배적인 호출 방식으로 활용되고 이다.

그럼 이제부터 동기 호출 방식에 몇 가지 질문을 해보자 우선 처리 시간의 관점에서 동기 호출을 살펴보자. 일반적으로 동기 호출은 즉시 답을 요구하는 실시간 애플리케이션에 적합한 호출 방식이다. 즉 동기 호출의 처리는 아주 짧은 순간에 처리를 마쳐야 하는 경우에 주로 사용된다. 그런데 동기 호출을 미시적으로 살펴보면 (여기에서는 단일 애플리케이션에서 동기 호출을 살펴본다.) 호출 명령에서 호출이 일어난 실행 주소를 보관하고 하부작업 주소로 실행 주소를 옮겨 하부작업을 실행하고 하부작업 실행 결과를 저장하고 다시 호출 실행 주소로 작업을 옮겨 실행을 계속하게 된다. 이 과정에서 하부작업의 실행 시간이 반드시 일정 시간 필요하게 되는데 결과적으로 하부작업 요청시각(t0)와 하부작업 리턴 시각(t1) 사이 Δt = t1 – t0 이 진행된 후 본작업은 다음 작업을 할 수 있게 된다. 다시 말해 동기 호출도 엄밀히 말하면 동시성을 보장하지 못하고 반드시 비 동시성을 전제하는 호출인 것이다. 이렇게 동기 호출에 대하여 하부작업 처리를 위한 처리 시간이 필요하다는 것을 이해 한다면 결국 동기 호출도 실행 흐름은 순차적이지만 호출에 따른 실행 시간은 비동시적인 것이다.

동기 호출의 가장 큰 특징은 실행 흐름의 제어가 손쉽다는 것이다. 즉 앞에서 말한 하부작업 호출과 그 결과 획득의 프로그램의 간결성이다. 동기 호출을 사용하면 애플리케이션은 하부작업 처리 결과를 획득하기 위하여 별다른 노력 없이 단순히 함수(메서드)만 호출하면 된다.
int in, out;
in = 1;
out = subprocess(in);
System.out.println(in + " => " + out);
위 Java 소스 세 번째 줄을 보면 본처리 프로그램에서 하부처리 메서드 호출과 그 처리 결과 획득과정이 단 한 줄로 표현되는 것을 볼 수 있다. 이와 같은 동기 호출의 사용은 단순한 구조를 가지고 있고 단일 애플리케이션에서 분산 애플리케이션까지 일관되게 적용된다. 즉 분산 애플리케이션에서도 단일 애플리케이션의 호출 방식과 같은 방식으로 동기 호출을 사용한다. 그러나 이런 편리한 동기 호출을 지원하기 위하여 분산 애플리케이션은 통신 프로토콜, 하부 라이브러리, 프레임워크 부분에서는 상당히 복잡한 동기화 작업들을 구현하고 있다.

그럼 분산 애플리케이션에서 동기 호출 시 하부작업의 처리에 문제가 발생하면 어떻게 될까? 만약 하부작업에서 흐름제어를 관리할 수 있는 정도의 문제가 발생하는 경우에는 하부작업은 오류를 리턴하면 된다. 그러나 만약 하부작업의 문제가 흐름제어를 관리할 수 없을 정도의 심각한 문제가 발생하여 하부작업이 리턴을 할 수 없게 되면 전체 실행 흐름은 정지하고 애플리케이션은 정지(Hang-up)상태가 된다. 그리고 요구된 거래는 성공도 실패도 알 수 없는 상태에 이르게 될 것이다. 이렇게 하부작업에 흐름제어를 잃는 장애가 발생하면 애플리케이션은 중대한 문제 상황에 빠지게 된다. 그렇지만 일반적인 동기 호출에서는 하부작업 호출 시 타임아웃을 설정할 수 없다. 만약 하부작업 호출 시 타임아웃을 설정하고 실행 흐름 제어를 회복하기 위해서는 호출 프로세스(스레드)에 하부작업 완료를 확인하는 별도 스레드 등을 추가해서 하부작업을 모니터링 하는 기능을 추가 해야 한다. 그러나 일반적인 동기호출 사용 패턴에서 이런 문제까지는 고려는 되지 않고 있다. 예를 들어 J2EE 동기 호출 규격에도 흐름제어 회복을 위한 타임아웃에 대한 고려는 없다. 그러나 애플리케이션 운영측면에서 동기 호출 방식의 하부작업 호출에서 흐름제어 회복을 위한 타임아웃을 고려하지 않는다면 정지된 본작업 프로세스(스레드)는 아무런 일을 하지 않는 좀비가 되고 시스템 자원만 점유하게 되어 시스템 자원 가용성 및 성능에 영향을 줄 수 있다. 그리고 하부작업 흐름제어 회복 불능 장애 발생 시 더 심각한 애플리케이션 비즈니스 처리 상태의 불확실성을 만들어 낼 것이다.

동기 호출 패턴이 타임아웃 처리에 대한 관심이 적은 이유는 과거 단일 시스템에서 단일 프로세스로 동작하는 애플리케이션에서 사용하던 함수 호출에 대한 경험 때문일 것이다. 단일 프로세스 환경에서는 프로세스 내 실행 흐름이 하나 밖에 없기 때문에 본작업이던 하부작업이던 모두 같은 실행 흐름 내에 존재한다. 그러므로 하부작업이 흐름제어를 회복할 수 없는 문제가 발생하면 이미 전체 애플리케이션의 문제이기 때문에 애플리케이션은 동일한 정지(Hang-up) 현상을 보이게 된다. 그러나 분산 애플리케이션에서는 본작업의 실행흐름을 가진 프로세스(스레드)와 하부작업의 흐름을 가진 프로세스(스레드)가 각각 독립적인 실행 흐름을 가질 수 있다. 그 결과 동기 호출 시 본작업에서 요청한 하부작업의 실행흐름이 불능인 경우에도 본작업 프로세스(스레드)의 실행 흐름은 영향을 받지 않고 다음 처리를 할 수 있다. 단 본작업이 하부작업의 실행흐름을 잃는 장애에 대하여 자신의 실행 흐름을 계속하기 위해서는 본작업은 하부작업의 처리시간을 한정하는 타임아웃 기능을 추가해야 한다. 그러나 앞서 말했듯이 동기 호출에 대한 타임아웃 고려는 아직 개별적 고려 사항으로 일부 라이브러리나 규격에서 설정 또는 패러미터 입력으로 등장하고 있는 정도이다.

분산 애플리케이션 환경에서 동기 호출의 안정성에 대한 문제를 살펴보자. 일반적으로 단일 하드웨어에서 단일 애플리케이션의 동기 호출은 충분히 신뢰할 수 있다고 볼 수 있다. 그러나 분산 애플리케이션 환경에서 동기 호출은 시스템 하드웨어, 네트워크 등 관련된 구성 요소들이 모두 충분히 신뢰할 수 있다는 전제하에 안정성을 기대할 수 있다. 그러나 분산 애플리케이션에서는 아래와 같은 경우 동기 호출에 대한 신뢰성 문제를 가질 수 있다.

  1. 복수 시스템에 각각 동작하는 애플리케이션들 사이 동기 호출이 발생할 경우 시스템과 시스템의 네트워크 안정성은 프로세스 내 단일 애플리케이션의 하부작업 호출에 사용되는 시스템 내부 버스의 안정성보다 상당히 낮다. 그리고 네트워크 장애 발생 시 동기 호출로 연동되는 일련의 애플리케이션은 모두 중단될 수 밖에 없다. 
  2. 복수 시스템에서 각 시스템의 애플리케이션이 동기 호출을 사용하는 경우 상대 시스템의 안정성을 기대할 수 없는 경우 가 있다. 다시 말해 각 시스템에 소유 주체가 다름으로 각 시스템에 동작하는 애플리케이션의 동작 여부를 항상 보장 받지 못할 수 있다. 어느 쪽이던 시스템의 장애 또는 애플리케이션에 장애가 발생하면 동기 호출로 연동되는 일련의 애플리케이션은 모두 중단될 수 밖에 없다.

동기 호출 메커니즘은 프로그램 언어가 등장한 이래 가장 일반적인 작업 메커니즘이었다. 그러나 하드웨어가 발전하고 시스템들은 네트워크를 통해 연결되고 단일 애플리케이션에서 분산 애플리케이션으로 작업 규모가 커지면서 동기 호출의 한계가 점점 등장하고 있다. 동시성 또는 아주 짧은 처리 시간을 요구하는 업무들을 처리하는 방식에 동기 호출은 적합하지만 동시성을 지원하기 위하여 시스템 안정성, 네트워크 안정성, 애플리케이션 안정성을 모두 보장해야 하는 어려움이 따르게 되었다. 이런 어려움 속에서도 동기 호출 방식의 패러다임은 여전히 가장 선호하는 호출 방식으로 세상을 지배하고 있다. 그러나 분산 애플리케이션 환경에서 동기 호출을 사용할 때 하부작업 처리 시간, 실행 제어 회복, 분산 구간의 안정성 등을 고려해야 하므로 동기 호출 패턴이 예전 단일 애플리케이션과 비교하여 그렇게 단순하지 않게 되었으며 이와 관련하여 대안적인 패턴으로 비동기 호출의 필요성이 재 발견되고 있다.

지금까지 기술적 측면에서 동기 호출에 대한 몇 가지 사항을 생각해 보았다. 그런데 우리의 일상적인 비즈니스 패턴은 어떤 패턴일까도 고민해 볼 필요가 있다. 우리들이 살아가는 세상은 동기 호출 방식이 지배하는 세상일까 비동기 호출 방식이 지배하는 세상일까? 우리는 개인, 조직, 국가, 세계와 어떻게 의사 소통하며 살아가는가에 대하여 좀더 고민해 볼 필요가 있다. 그래야 동기, 비동기 문제를 좀더 객관화시킬 수 있고, 우리 생활과 잘 접목되는 애플리케이션 사용 방향을 찾을 수 있을 것이다.

2012년 8월 14일 화요일

동기 호출(Synchronous Call), 비동기 호출(Asynchorous Call)


프로그램에서 동기, 비동기 호출은 멀티 프로세스, 멀티 스레드 환경에서 동작하는 애플리케이션을 작성할 때 등장한다. 멀티 프로세스(스레드) 환경 즉 복수 프로세스(스레드)가 병렬적으로 동시에 실행되는 환경에서 각 프로세스(스레드)는 독립적인 실행 흐름을 가지고 동작하는데, 한 프로세스(스레드)가 다른 프로세스(스레드)에게 하부작업(프로시저, 함수, 메서드)을 요청할 경우 호출한 프로세스(스레드)의 실행 흐름의 중지 여부에 따라 따라 동기, 비동기를 구분 짓게 된다. 이 두 개념을 좀더 상세하게 설명하면 다음과 같다.

애플리케이션 프로그램에서 동기 호출(Synchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업(프로시저, 함수, 메서드) 요청 시 요청된 하부작업이 진행되는 동안 호출 프로세스(스레드)의 실행 흐름이 멈추게 되는 호출을 말한다. 멈춘 호출 프로세스(스레드)의 실행 흐름은 하부작업이 리턴되면 다시 계속된다.



애플리케이션 프로그램에서 비동기 호출(Asynchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업 요청 시 요청된 하부작업의 실행 또는 종료와 관계없이 호출 프로세스(스레드)의 실행 흐름은 계속되는 호출을 말한다. 이런 호출 방식에서는 하부작업의 실행완료 시점을 호출 프로세스(스레드)가 정확이 알지 못하므로 호출 프로세스(스레드)와 하부작업은 하부작업의 실행 결과를 둘 사이 약속된 결과 영역 조회나 하부작업이 호출 프로세스(스레드)를 호출하는 Callback 메커니즘을 통해 확인한다.



비동기 호출은 기업 통합 패턴(Enterprise Integration Patterns)을 이해하는 기본 개념이다. 기업 통합 패턴은 비동기 호출을 어떻게 기업 애플리케이션 아키텍처로 활용할 수 있는지 설명한 패턴으로 기업 시스템이 복잡해짐에 따라 애플리케이션은 애플리케이션 내 상호 작용, 애플리케이션 간 상호 작용을 위한 효과적인 아키텍처가 필요한대 이때 기업 통합 패턴이 유용하게 사용될 수 있다.

참고)
1) Hophe Gregor and Bobby Woolf. Enterprise Integration Patterns (Addison-Wesley, 2003)

2012년 8월 3일 금요일

설정자 패턴(Configurer pattern)


어떻게 오브젝트 생성 시 필요한 초기화 정보의 획득과 입력 작업을 오브젝트 생성 로직에서 분리할 수 있을까?

일반적으로 오브젝트 생성과 관련하여 개발자들의 관심사는 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