Framework/iBatis2008. 3. 25. 20:53

Java로 진행되는 많은 프로젝트들이 iBatis를 이용하여 진행될 것이다. iBatis를 사용해본 사용자라면 iBatis를 사용하기 위해 생성해야 하는 DTO, DAO, SqlMap들을 만드는 것이 귀찮은 작업이라는 것은 다들 느낄 것이다. 나또한 DTO를 생성하는 것이 제일 귀찮으니 ...
그래서 iBatis 관련 소스 Generator를 만들까 고민하다 iBatis에서 제공하는 Abator이란 것을 발견하고 간단히 사용해 보니 쓸만해서 소개를 한다. 물론 생성 된 소스를 조금은 수정을 해야 한다.

그럼 Abator를 간략하게 들여다 보자.

-- Abator 다운로드 
Abator 공식 사이트에서 소스와 Documents, Binaries를 다운받아 적당한 위치에 압축을 푼다.

-- iBatis 소스 생성하기
Abator Plugin은 간단하게 말하면 DataBase에 접속하여 지정 된 Table에 대한 DTO, Key, DAO, SqlMap 소스를 자동으로 생성해 준다. Abator을 이용하여 iBatis 소스를 생성하는 방법은 간단하다. abator의 config xml file을 생성하고 ant를 이용하여 실행하면 된다.

abator_config.xml file
   <?xml version="1.0" encoding="UTF-8"?>
   <!DOCTYPE abatorConfiguration PUBLIC "-//Apache Software Foundation//DTD Abator for iBATIS Configuration 1.0//EN" "http://ibatis.apache.org/dtd/abator-config_1_0.dtd">
   <abatorConfiguration>
      <abatorContext id="Oracle">
         <!-- DataBase 연결정보 정의 -->
         <jdbcConnection driverClass="oracle.jdbc.driver.OracleDriver"
               connectionURL="jdbc:oracle:thin:@192.160.100.204:1521:devora"
               userId="backend" password="!Qprdpsem@">
               <classPathEntry location="xxx/ojdbc14.jar" />
         </jdbcConnection>
         <javaTypeResolver>
            <property name="forceBigDecimals" value="false" />
         </javaTypeResolver>

         <!-- 모델빈의 위치및 생성옵션 (DTO 생성) -->
         <javaModelGenerator targetPackage="com.junducki.blog.ibatis.dto" targetProject="../blog/src/main/java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
         </javaModelGenerator>
         <!-- SqlMap 생성 -->
         <sqlMapGenerator targetPackage="pay" targetProject="../blog/src/main/java/sql">
            <property name="enableSubPackages" value="true" />
         </sqlMapGenerator>
         <!-- DAO 생성옵션 -->
         <daoGenerator type="SPRING" targetPackage="com.junducki.blog.ibatis.dao"
               targetProject="../blog/src/main/java">
            <property name="enableSubPackages" value="true" />
            <property name="useActualColumnNames" value="false" />
         </daoGenerator>
 
         <!-- 소스생성에 필요한 테이블들을 지정한다. -->
         <table tableName="TB_PAY_INFO" domainObjectName="PayInfo" enableDeleteByExample="false"
               enableSelectByExample="false" enableUpdateByPrimaryKey="false" enableInsert="false">
            <property name="useActualColumnNames" value="false" />
            <property name="createDynamicSQL" value="true" />
         </table>
      </abatorContext>
   </abatorConfiguration>

build.xml file
   <?xml version="1.0" encoding="UTF-8" ?>
   <project name="blog" default="geniBatis" basedir=".">
      <property name="generated.source.dir" value="${basedir}" />
      <target name="geniBatis" description="Generate iBatis">
         <taskdef name="abator" classname="org.apache.ibatis.abator.ant.AbatorAntTask"
               classpath="lib/abator.jar" />
            <abator overwrite="true" configfile="../ibatis_plugin/abator_config.xml" verbose="false">
            <propertyset>
               <propertyref name="generated.source.dir" />
            </propertyset>
         </abator>
      </target>
   </project>

위의 abator_config.xml과 build.xml 파일을 이용하여 생성한 iBatis 소스들은 아래와 같다.

사용자 삽입 이미지

[ Abator을 이용해 iBatis 소스 생성 결과 ]

DAO : PayInfoDAO, PayInfoDAOImpl
DTO : PayInfo, PayInfoKey
SqlMap : TB_PAY_INFO_SqlMap.xml

이와 같이 Abator를 이용하면 아주 간단히 iBatis 관련 소스를 실제 DataBase와 매핑하여 생성할 수 있다. 그리고, 여기서는 아주 간단하게 Abator을 다루어 보았는데, Abator Document를 보면 configuration 설정에 더욱 많으니 꼭 보기 바란다.

Posted by as.wind.914
Framework/iBatis2008. 3. 18. 20:13

개발자들이 Application에 도입하는 대부분의 캐싱은 오랜 시간동안 변경되지 않는 데이터를 위한 것이다. 그러나 의외로 캐싱된 데이터가 쓰기에 의해 변경되는 경우도 있다.
개발자들은 나름대로 캐싱모델을 정의하고, 구현하여 사용한다. 그러나 캐싱 될 데이터를 추가하고, 더 이상 사용되지 않는 데이터는 삭제하고, 정해진 시간별로 캐싱 된 데이터를 삭제하는 등의 많은 기능들을 정의하고 구현하여 사용하려고 하는 솔직히 쉬운 일도 아니고, "노력 대비 성능" 또한 보장할 수가 없다.
그래서 요즘 많은 ORM 솔루션들이 데이터 캐싱을 지원한다. iBatis 또한 여타 ORM 솔루션과 같이 데이터 캐싱을 지원한다. 그러나 iBatis의 데이터 캐싱은 다른 ORM 솔루션의 데이터 캐싱과는 조금 개념이 다르다. 여타의 ORM 솔루션들은 주로 DataBase Table 객체에 매핑하는 데 중점을 두고, iBatis는 SQL 구문을 객체에 매핑하도록 되어 있다.

-- CacheModel 이해하기
CacheModel은 iBatis의 모든 캐시 구현체를 정의하는 기반이 되는 곳이다. SQL Maps 설정 안에서 캐시 모델 설정을 정의하고 하나 이상의 쿼리 매핑 구문이 이를 사용할 수 있다.

* CacheModel 속성들 *
id (필수) : 유일한 ID를 지정. CacheModel에 설정 된 캐시를 사용하고 하는 쿼리 매핑 구문에서 ID를 참조한다.
type (필수) : 이 값은 CacheModel이 설정하는 캐시의 타입을 의미한다. 사용 가능한 값으로 MEMORY, LRU, FIFO, OSCACHE가 있다. 이 속성은 사용자 정의 CacheController 구현체의 완전한 클래스 이름으로 지정해도 된다.
readOnly (선택) : 읽기전용 여부. 이 값을 true로 지정하면 캐시가 읽기 전용 캐시로 사용될 것임을 의미한다. 읽기 전용 캐시에서 가져온 객체는 객체의 Property들을 변경할 수 없다.
serialize (선택) : 캐시의 내용을 가져올 때 객체의 모든 값을 복사하여 새로운 객체를 생성하여 전달할지 여부를 지정한다.

여기서 readOnly 속성 값에 대해 좀더 알아보면 readOnly 속성은 단순히 CcheModel에게 캐시된 객체를 어떻게 가져와서 저장할지 알려주는 지시자이다. 이 속성값이 true이면 가져온 내용을 변경해도 CacheModel은 변경되지 않는다. 만약 false로 지정되면 주의해야 하는데 두 명 이상의 사용자가 캐시된 참조의 동일한 인스턴스를 가져갈 수 없음을 의미한다.

* 내장 CacheModel 타입들 *
MEMORY : 단순하게 캐시된 데이터를 가비지 컬렉터가 삭제할 때까지 메모리에 저장한다.
FIFO : 고정 된 크기의 모델로 "first in first out (먼저 들어간 값을 먼저 삭제)" 알고리즘을 사용하여 메모리에서 캐시 항목들을 삭제한다.
LRU : FIFO와 다른 고정 된 크기의 모델로 "least recently used (최근에 가장 오랜동안 사용하지 않은 값을 캐시에서 먼저 삭제)" 알고리즘을 사용하여 메모리에서 캐시 항목들을 삭제한다.
OSCACHE : OpenSymphony Cache를 사용한다. (OSCashe Lib와 설정이 함께해야 한다.)

* readOnly와 serialize 속성의 조합에 대한 요약 *
readOnly (true) / serialize(false) : 성능 좋음. 캐시 된 객체를 가장 빠르게 가져온다. 캐시 된 객체의 공유 인스턴스를 반환하며, 잘못 사용하면 문제를 일으킬 수도 있다.
readOnly (false) / serialize(true) : 성능 좋음. 캐시 된 객체를 빠르게 가져온다. 캐시 된 객체를 깊은 복사 작업을 통해 가져온다.
readOnly (false) / serialize(false) : 주의요망! 캐시는 오직 호출하는 스레드의 세션이 살아있는 동안에만 관련되고, 다른 쓰레드는 사용할 수 없다.
readOnly (true) / serialize(true) : 성능 나쁨. 무의미한 설정이라는 점을 제외하면 readOnly (true) / serialize(true)와 동일하게 작동한다.

readOnly=true이고 serialize=false이면 캐시 된 객체가 전역적으로 공유되기 때문에 모든 사용자는 다른 세션에서 부적절하게 변경 된 객체를 가져오게 될 수 있는 문제의 소지가 있다. readOnly=false이고 serialze=true를 사용하면 캐시에서 가져온 객체의 깊은 복사작업의 결과를 가져오기 때문에 캐시에서 가져온 객체가 값은 비록 같지만 동일한 인스턴스가 아니라는 의미이다. 이것은 캐시에서 가져온 객체의 변경사항이 호출한 세션 안에서만 작용된다.

-- 캐시 비우기 (Cache Flushing)
사용자가 정의 한 CacheModel은 캐시된 데이터를 모두 비울때 사용하는 공통적인 요소를 가지고 있다. 이것은 이전에 캐시된 모든 데이터를 비우고 새롭게 데이터를 캐시하게 된다.

* 캐시를 비우는 flush 요소들 *
<flushOnExecute> : 지정 된 쿼리 매핑 구문이 실행되면 캐시의 모든 데이터를 비운다.
<flushInterval> : 캐시를 비우는 시간 간격을 정의한다.

<flushOnExecute>
속성으로 statement 하나만을 가지며, 이 속성에 지정된 매핑 구문이 실행이 될 때 자동으로 캐시의 데이터를 모두 비운다. 예를 들어 상품카테고리의 카테고리가 추가, 수정, 삭제 될면 이전에 캐시된 신빙성이 떨어지는 데이터를 모두 비우게 할 수 있다.
그러나, 이 속성은 캐시된 데이터를 모두 비우고 새롭게 캐시를 하게 되므로 데이터를 자주 변경하는 매핑 구문에 의존하게 되면 캐시의 효율성이 떨어지게 된다.

   <SqlMap namespace="category">
      <cacheModel id="categoryCache" type="MEMORY">
         ...
         <flushOnExecute statement="category.insert" />
      </cacheModel>
      ...
      <insert id="insert" parameterClass="java.util.Map">
         ...
      </insert>

<flushInterval>
<flushOnExecute> 보다 좀더 간편하게 사용할 수 있는 요소로 시간에 의존하여 지정한 시간이 경과되면 반복적으로 캐시된 데이터를 모두 비운다. 가격은 시(hours),분(minutes),초(seconds) 또는 밀리초(milliseconds)로 지정할 수 있다. <flushInterval>은 하나의 속성만을 허용하기 때문에 5시간 13분 40초와 같이 지정하고 싶다면 초로 계산하여 지정해야 한다. 그리고, 특정 시간을 지정할 수는 없다.

   <cacheModel id="categoryCache" type="MEMORY">
      ...
      <flushInterval hours="12" />
   </cacheModel>

-- CacheModel Properties 설정하기
CacheModel은 플러그인의 형태로 제공되기 때문에 임의의 설정 값을 제공할 수 있는 방법이 필요한데 <property>를 이용하여 처리한다.

* <property> 요소의 속성들 *
name : 필수입력으로 설정할 프로퍼티의 이름
value : 필수입력으로 설정할 프로퍼티의 값

-- CacheModel Type
앞에서 언급했듯이 iBatis에서 사용할 수 있는 CacheModel Type은 MEMORY, LRU, FIFO, OSCACHE 4가지가 있다.

MEMORY
객체 참조를 기반으로 한 캐시이다. 캐시 내의 각 객체의 참조 타입(<reference-type>) 속성을 갖고 있다. 객체의 참조 타입은 가비지 켈렉터에게 객체를 어떻게 다룰지에 대한 힌트를 제공한다. MEMORY CacheModel은 객체에 접근하는 방식보다는 메모리 관리에 중점을 둔 Application에 적합하다.

* MEMORY CacheModel 참조 타입 *
WEAK : 캐시된 데이터를 빨리 비운다. 기본설정 값이며 가비지 켈렉터에 의해 수거되는 것을 막지않고 놔둔다. 이 방식은 일관성 있게 객체에 접근하는 캐시를 사용할 때 잘 적동한다. 캐시를 비우는 비율이 빠른편이기 때문에 메모리 제한을 넘기지 못하도록 보장은 하나 DataBase 접근확률이 높아진다.
SOFT : 메모리 용량이 허락하는 한 캐시된 객체를 보관한다. 가비지 켈렉터는 더 많은 메모리가 필요하다고 판단되기 전까지는 이 객체들을 수거하지 않는다. 메모리 제한을 넘기지 못하도록 보장하며, WEAK 참조보다 DataBase 접근확률은 적은 편이다.
STRONG : 메모리의 한계가 얼마든지 관계없이 객체를 계속 보관한다. 가비지 켈렉터에 의한 수거는 없다. 이 참조타입은 정적이고 작고 장기적으로 사용할 객체를 캐시할 때 유용하다. DataBase 접근확률은 최소할 수 있으나, 캐시되는 객체의 용량이 너무 커져서 메모리 부족이 발생될 수 있다.

   <cacheModel id="categoryCache" type="MEMORY">
      <flushInterval hours="12" />
      <flushOnExecute statement="insert" />
      <property name="reference-type" value="WEAK" />
   </cacheModel>

LRU
가장 최근에  가장 오랫동안 사용되자 않은 것을 제거하는 방식으로 캐시를 관리한다. 캐시의 객체를 제거하는 것은 오직 캐시의 용량이 제한을 넘겼을 때 한번만 발생된다. 특정 객체에 아주 빈번하게 접근하는 캐시를 관리할 때 매우 적합하다. <property> 요소를 사용하여 지정할 수 있는 프로퍼티는 size 하나로 캐시에 저장될 수 있는 최대 개수를 지정한다.

   <cacheModel id="categoryCache" type="LRU">
      <flushInterval hours="12" />
      <flushOnExecute statement="insert" />
      <property name="size" value="200" />
   </cacheModel>

FIFO
먼저 캐시 된 객체를 먼저 삭제한다. 캐시의 객체를 제거하는 것은 오직 캐시의 용량이 제한을 넘겼을 때 한번만 발생된다. 생존기간에 기반을 두고 있기 때문에 초기에 캐시에 저장되는 그 순간에 더 많이 사용되는 객체를 캐싱할때 효과적이다. <property> 요소를 사용하여 지정할 수 있는 프로퍼티는 size 하나로 캐시에 저장될 수 있는 최대 개수를 지정한다.

   <cacheModel id="categoryCache" type="FIFO">
      <flushInterval hours="12" />
      <flushOnExecute statement="insert" />
      <property name="size" value="200" />
   </cacheModel>

OSCACHE
Open Symphoy(http://www.opensymphony.com/oscache)의 OSCache를 사용한다.OSCACHE를 사용하기 위해서는 OSCache 라이브러리와 설정정보가 필요하다.

  <cacheModel id="categoryCache" type="OSCACHE">
      <flushInterval hours="12" />
      <flushOnExecute statement="insert" />
   </cacheModel>

사용자가 만든 캐시 모델
앞에서 언급했듯이 iBatis의 캐시 모델은 플러그인되는 형태여서 사용자가 캐시모델을 만들어서 사용할 수 있다. 사용자가 자신의 캐시모델을 만들기 위해서는 com.ibatis.sqlmap.engine.cache.CacheController 인터페이스를 구현하면 되고, 이름은 alias를 사용하면 된다.

Posted by as.wind.914
Framework/iBatis2008. 3. 10. 16:35
얼마전 프로젝트를 진행 중 iBatis에서 Procedure를 사용하고 Transaction을 Commit을 했으나, Commit이 되지 않고 모두 Rollback이 되는 현상이 발생을 했다.
아직 이 문제가 발생하는 근본 원인은 파악을 하지 못했고, 해결 방안만 찾아 사용했다. 해결 방안을 간단하게 정리한다. 소개는 실제 사용한 소스가 아니니 참고하세요.

-- 환경 설정
iBatis : iBatis 2.3.0
DB : Oracle 10g
Framework : Webwork 2.2.6, Spring 2.0.7

-- Commit 되지 않는 문제 발생
iBatis를 통해 실행 할 Procedure는 아래와 같이 TB_EMP 테이블에 NO와 NAME을 받아서 INSERT하는 Procedure이다. 그리고, Procedure 내에서 Transaction을 관리하지 않고, iBatis에서 Transaction을 관리하게 구성되어 있다.

Oracle Procedure :
   CREATE OR REPLACE PROCEDURE SP_TEST
      (p_no IN TB_EMP.NO%TYPE, p_name IN TB_EMP.NAME%TYPE)
   IS
   BEGIN
      INSERT INTO TB_EMP (NO, NAME) VALUES (p_no, p_name);
   END;

iBatis SqlMap :
   <parameterMap id="empMap" parameterClass="java.util.Map">
      <parameter property="no" mode="IN" jdbcType="DECIMAL" javaType="int" />
      <parameter property="name" mode="IN" jdbcType="VARCHAR" javaType="java.lang.String" />
   </parameterMap>
   <procedure id="insertEmpInfo" parameterMap="empMap" >
      { CALL SP_TEST(?, ?) }
   </procedure>

Execute Java Source :
   private SqlMapClient sqlMapClient = null;

   public void insertEmpInfo(int no, String name) throws SQLException {
      java.util.Map<String, Object> conditionMap = new java.util.HashMap<String, Object>();
      conditionMap.put("no", no);
      conditionMap.put("name", name);

      try {
         // start transaction
         sqlMapClient .startTransaction();
         
         sqlMapClient.queryForObject("insertEmpInfo", conditionMap);

         // commit transaction
         sqlMapClient.commitTransaction();
      } finally {
         // rollback and end transaction
         sqlMapClient.endTransaction();
      }
   }

위와 같은 코드에서는 Exception이 발생하지 않고 commitTransaction()이 실행 되었음에도 실제 DataBase에는 commit이 되어 있지 않고 rollback이 되어 있다.
그러나 같은 Transaction에서 Procedure와 함께 INSERT, UPDATE, DELETE Query가 실행이 되면 commit은 정상적으로 된다. 함께 실행되는 Query가 Procedure 전, 후 어디에서 실행이 되어도 상관은 없다. 왜 commit 되지 않는 것인가 원인은 아직 찾지를 못했고, 해결 방안만을 찾았다.

-- 해결 방안
   public void insertEmpInfo(int no, String name) throws SQLException {
      java.util.Map<String, Object> conditionMap = new java.util.HashMap<String, Object>();
      conditionMap.put("no", no);
      conditionMap.put("name", name);

      try {
         // start transaction
         sqlMapClient .startTransaction();
         
         sqlMapClient.queryForObject("insertEmpInfo", conditionMap);

         // commit transaction
         sqlMapClient.commitTransaction(); è sqlMapClient.getCurrentConnection().commit();
      } finally {
         // rollback and end transaction
         sqlMapClient.endTransaction();
      }
   }

위와 같이 sqlMapClient의 Session에 저장 된 Connection을 commit() 하는 것이 아니라 sqlMapClient의 현재 User Connection을 가져와 강제로 commit()을 해주면 INSERT, UPDATE, DELETE Query를 같이 사용하지 않는 경우에도 commit이 정상 처리 된다.

이 문제는 일부 JDBC Driver에서만 발생하는 문제인지, iBatis의 문제인지는 아직 확인을 하지 못 했다.
Posted by as.wind.914