[출처] http://lang.hessie.net/2

1. 오라클 데이터베이스 JDBC 드라이버의 버그

(2008년 6월 30일 오라클에서 배포하는 ojdbc14.jar를 기준으로) 오라클 JDBC 드라이버를 이용하다 보면, CLOB 타입 컬럼에 대한 업데이트를 수행하는 중에 불규칙적으로 수많은 종류의 오류를 발생시키는 것을 볼 수 있다.
우선 예제를 하나 살펴보자. 아래의 예제는 공지사항 테이블에 신규 내용을 입력하는 쿼리이며 iBATIS 2.3 버전용으로 작성되었다. 이 기사에서는 계속해서 동일한 버전으로 설명할 것이다.

Listing 1. sqlMap 쿼리 XML 예제
<?xml version="1.0" encoding="euc-kr" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
  "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="Notice">

  <insert id="insert">
    insert into NOTICE (SEQ, USERID, TITLE, CONTENTS, REGDATE)
    values (#seq:NUMERIC#, #userId:VARCHAR#, #title:VARCHAR#
      , #contents:CLOB#, sysdate)
  </insert>

  <update id="update">
    update NOTICE
    set TITLE = #title:VARCHAR#, CONTENTS = #contents:CLOB#
    where SEQ = #seq:NUMERIC#
  </update>

   ......

</sqlMap>

NOTICE 테이블의 CONTENTS 컬럼이 CLOB 타입이라는 것 외에는 별로 특이할 것도 없는 단순한 INSERT/UPDATE 구문이다. 더욱이 이 쿼리를 사용하는 자바 코드는 단위테스트를 마칠 때까지 아주 잘 동작하기까지 한다. 그러나 막상 통합테스트와 인수테스트를 거치기 시작하면, 그전까지 전혀 예상할 수 없었던 기가 막힌 오류들을 SQLException 형태로 쏟아내고 만다. 여기에는 발생 가능한 오라클의 에러 메시지들을 일일히 나열하지 않겠다. 그 내용들이 사실은 무의미한 것들이기 때문이다. 그 다양한 오류들은 다름 아닌 오라클 JDBC 드라이버의 한 버그로부터 오는 것이다.

 

2. 오라클에서 CLOB INSERT/UPDATE시 오류가 발생하는 이유

CLOB을 다루면서 단위테스트를 진행하는 동안 오류가 발견되지 않은 이유는, 테스트에 사용하는 데이터에 기인한다. 개발자는 단순히 '123'이라는 데이터를 가지고 입력하고 수정하면서 테스트를 수행하기 때문이다. 그것은 너무나 짧다. 충분히 긴 데이터를 가지고 테스트를 진행할 때에라야 비로소 오라클 JDBC 드라이버는 작정한 것처럼 자체 버그 속에서 헤매기 시작한다. 개발자들을 도무지 믿을 수 없다는 듯, PreparedStatement에 접근하는 열번호를 열심히 계산해서 새롭게 할당하기 시작한다. 다시 말해, 앞의 예제에서 contents에 할당하려는 데이터를 자기 마음대로 seq나 title에 할당하고는 입을 닦아 버리는 것이다. 그것이 어디로 튈 지는 아무도 모른다. 이제 감이 좀 잡히는가? 이 오류들이 iBATIS와는 전혀 관련이 없다는 말씀이다.

 

3. 가장 좋은 해결책 구하기

사실 CLOB 데이터는 신참 개발자가 주로 다룬다. 널린 게 게시판이라는 인식 때문인 것 같다. 숙련된 개발자가 와서 고민에 쌓인 신참 개발자를 보고는 뒷통수를 냅다 후려칠 수도 있다. 그러고 나서는 아마도 자랑스럽게 해결책을 제시해 줄 것인데, 그 해결책이란 게 십중팔구는 아래와 같다. 이는 오라클 문서에서 권장하는 내용이기도 하다.

* 숙련 개발자가 일반적으로 제시하는 CLOB INSERT/UPDATE 해결법 *

a) insert/update 구문에서 CLOB 컬럼에는 empty_clob()을 전달해라.
b) 방금 insert/update한 CLOB 컬럼을 select 하는 쿼리를 추가로 작성해라. 단, for update는 필수.
c) 그 컬럼을 getClob으로 건져와서 지체하지 말고 스트림으로 쏴라.

서광이 비친다. 이제 할 일이 많아졌다. SQL도 새로 짜 넣어야 하고, 소스 코드도 뜯어 고쳐야 하고, Q&A를 맡은 동료 개발자에게도 알려줘야 한다. 오라클 문서를 뒤져 보면, 이 외에도 CLOB 데이터를 다루는 수많은 방법들이 나열되어 있다. 어떤 방법이든 간에, 업무 로직만으로 간결하게 짜여져 있던 소스 코드는 CLOB을 처리하는 보다 많은 코드가 덕지덕지 붙기 시작한다. 기존의 간결한 코드를 유지할 수는 없을까? 차라리 JDBC를 역컴파일해서 디버깅을 해 보는 건 어떨까?

 

4. 해결 방법

JDBC를 뜯어고친다는 것은 매우 위험한 발상이다. 당신이 개발을 끝낸 후 나가고 6개월이 지날 무렵의 어느 날, 시스템 운영자가 무슨 생각에서인지 JDBC를 업그레이드해야겠다는 생각을 해내고는 지체없이 실천에 옮길 수도 있기 때문이다. 갑자기 CLOB 관련 쿼리들이 이상을 일으키기 시작할 것이고, 앞서 설명한 것과 마찬가지로 쏟아지는 SQLException들은 제대로 된 단서를 주지 않을 것이다. 다행히 운좋게도 이전의 JDBC 백업본을 분실하지 않았다면 그나마 반나절 정도의 장애 후에 원복시킬 수는 있을 것이다. 그렇더라도 수정한다는 전제만 없다면 JDBC를 역컴파일해서 버그 부분을 찾아보는 것이 의외의 소득을 줄 지도 모른다. 이미 필자가 뚜껑을 열어 보았고, 나름 가장 좋은 해결책이라고 생각하는 방법을 적용해 보았다. 그러나 너무 믿지는 말도록. "우리 고참 개발자들은 여전히 CLOB을 자주 다루지는 않으니까."

기존의 쿼리와 소스코드는 변경 없이 동일하게 사용할 것이다. 수정할 부분은 sqlMapConfig이다. 그 전에 핸들러를 하나 만들어 놓아야 한다.

Listing 2. OracleClobStringTypeHandler.java
/*
 * @(#)OracleClobStringTypeHandler.java    1.00 08/06/30
 *
 * Copyright 2008 hessie. All rights reserved.
 *
 */

package net.hessie.lang.support.ibatis2.type;

import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import oracle.sql.CLOB;

import com.ibatis.sqlmap.engine.type.BaseTypeHandler;
import com.ibatis.sqlmap.engine.type.TypeHandler;

public class OracleClobStringTypeHandler extends BaseTypeHandler
        implements TypeHandler {

    public void setParameter(PreparedStatement ps, int i, Object parameter
            , String jdbcType) throws SQLException {
        String value = (String) parameter;
        if (value != null) {
            CLOB clob = CLOB.createTemporary(ps.getConnection(), true
                , CLOB.DURATION_SESSION);
            clob.putChars(1, value.toCharArray());
            ps.setClob(i, clob);
        } else {
            ps.setString(i, null);
        }
    }

    public Object getResult(ResultSet rs, String columnName)
            throws SQLException {
        Clob clob = rs.getClob(columnName);
        if (rs.wasNull()) {
            return null;
        } else {
            return clob.getSubString(1, (int) clob.length());
        }
    }

    public Object getResult(ResultSet rs, int columnIndex)
            throws SQLException {
        Clob clob = rs.getClob(columnIndex);
        if (rs.wasNull()) {
            return null;
        } else {
            return clob.getSubString(1, (int) clob.length());
        }
    }

    public Object getResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        Clob clob = cs.getClob(columnIndex);
        if (cs.wasNull()) {
            return null;
        } else {
            return clob.getSubString(1, (int) clob.length());
        }
    }

    public Object valueOf(String s) {
        return s;
    }

}

이제 핸들러가 준비됐으니, sqlMapConfig 설정 파일을 열어서 핸들러를 추가해 주자.

Listing 3. sqlMapConfig 설정 XML
<?xml version="1.0" encoding="euc-kr" ?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map
  Config 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>

   ......

  <typeHandler javaType="string" jdbcType="CLOB"
callback="net.hessie.lang.support.ibatis2.type.OracleClobStringTypeHandler" />

   ......

</sqlMapConfig>

typeHandler 요소는 transactionManager 요소 바로 앞에 위치해야 한다. OracleClobStringTypeHandler 클래스의 setParameter 함수 로직을 보면 알겠지만, iBATIS가 아닌 일반 JDBC 코드에도 오라클 CLOB을 업데이트할 때 언제든지 응용이 가능하다. 그러나, 이런 고민이나 팁 없이도 누구나 쉽게 CLOB이나 BLOB을 다룰 수 있도록 오라클이 빠른 시일 내에 버그를 바로잡아 주기를 바란다.

Posted by gt1000

블로그 이미지
gt1000

태그목록

공지사항

어제
오늘

달력

 « |  » 2024.4
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함