본문 바로가기

개발 이야기/Java

Java JDBC Performance Tip-Insert, Delete Speed up!

벌써 작성한지 13년이나 된 글이네요. 추억 겸 올려봅니다.


Java JDBC Performance Tip-Insert, Delete Speed up!


이전 아티클에서는 database select speedup에 대하여 알아보았다. 이번 편에서는 update를 제외한 insert, delete tranaction의 속도를 최대로 끌어올릴 수 있는 방안을 살펴봅니다.


▶ Insert, Delete Query speed up!


JDBC 2.0에서는 기본적으로 scrollable한 메소드등의 유연성있는 메소드들과  batch processing에 관련된 메소드가 추가되었습니다.


그게 바로 어떤 것이냐하면 Statement, PreparedStatement 인터페이스의 addBatch(String sql), addBatch()메소드입니다. Pro*C같은 경우 기본적으로 array processing을 이용하여 데이터베이스에

작업을 하므로 상당한 퍼포먼스를 낼 수 있는데 이를 자바측으로 변환한것이 바로 addBatch메소드인 것이죠.


기본적으로 java에서도 배치기능을 사용하고자 한다고 하고, 여러분이 만약 드라이버 개발자라고 하면 어떻게 데이터베이스에 작업을 할까요?


그나마 setAutoCommit(false)를 connection에 때려넣어놓으면 commit에 대한 그만큼의 비용이 떨어질 것이라는걸 알고 있다면 다행입니다. 자. 그러면 데이터를 소위 "밀어넣는다"라고 이야기했을 때 가장 최선책은 무엇일까요? 건당 하는 건 무리일테고, 음~ 자바니까 Collection을 이용해보면 적당하겠네요.


그리고 Collection을 이용한다면 들어오는 데이터는 중복을 허용하고 순서도 있어야 할테니까 List 계열을 사용하는 것을 좋겠고 놀새의 결론은 List중에서도 제일 퍼포먼스가 좋은 ArrayList를 사용하는게 딱일것이라고 생각했습니다. 하지만 실제 Driver는 어떠한 자바버젼에서도 맞아야 하기 때문에 1.2부터 사용되는 ArrayList는 좀 고려해 볼만도 합니다.


그러면 뭐가 들어오는 sql문장에 대한 저장소로 적당할까요? 답은 Vector!! 왜 Vector인지는 설명하지 않겠습니다다. 


우선 그러면 실제 Oracle을 예로 driver내부나 한번 보도록 할까요?


JDBC API Statement 인터페이스의 구현체인 OracleStatement코드의 addBatch메소드를 잠깐 보도록 하죠.



    public synchronized void addBatch(String s)
        throws SQLException
    {
        addBatchItem(s);
    }

    private void addBatchItem(String s)
    {
        m_batchItems.addElement(s);
    }
addElement를 사용하는 걸 보니 놀새가 생각했던 것처럼 Vector를 사용하고 있습니다. 즉 batch를 위한 저장소로서 Vector class를 사용중인 것이다. 그러면 Statement클래스의 실제 batch execute 명령어인 executeBatch()메소드를 보도록 하죠.
    
public int[] executeBatch()
        throws SQLException
    {
        synchronized(connection)
        {
            synchronized(this)
            {
                int i = 0;
                int j = getBatchSize();
                if(j <= 0)
                {
                    int ai[] = new int[0];
                    return ai;
                }
                int ai2[] = new int[j];
                Object obj = null;
                Object obj1 = null;
                Object obj2 = null;
                boolean flag = false;
                ensureOpen();
                prepare_for_new_result(true);
                try
                {
                    connection.needLine();
                    for(i = 0; i < j; i++)
                    {
                        String s = getBatchItem(i);
                        String s1 = expandSqlEscapes(s);
                        byte abyte0[] = strToDbaccessBytes(s1);
                        byte byte0 = getSqlKind(s1);
                        if(byte0 == 0)
                            DBError.throwBatchUpdateException(80, "invalid SELECT batch command " + i, i, ai2);
                        ai2[i] = parseExecuteFetchWithTimeout(dbstmt, byte0, abyte0, null, 1, null, 1);
                        if(ai2[i] < 0)
                            DBError.throwBatchUpdateException(81, "command return value " + ai2[i], i, ai2);
                    }

                }
                catch(IOException ioexception)
                {
                    DBError.throwBatchUpdateException(81, ioexception.getMessage(), i, ai2);
                }
                catch(SQLException sqlexception)
                {
                    if(sqlexception instanceof BatchUpdateException)
                        throw sqlexception;
                    DBError.throwBatchUpdateException(81, sqlexception.getMessage(), i, ai2);
                }
                finally
                {
                    clearBatchItems();
                }
                int ai1[] = ai2;
                return ai1;
            }
        }
    }

위에서 보면 당연히 connection은 동기화되어져야 하므로 synch걸어놓고 작업할게 뻔하며, parseExecuteFetchWithTimeout메소드가 실제 update작업을 이루게끔 하는데 저 코드는 timeout이 
걸려있으면 타이머 작동시키고 statement에 update때리는 작업을 하며, timeout이 0이면 바로 update를 하게 합니다. 복잡한가요? 

PreparedStatement(이하 PS)의 addBatch()메소드는 조금 더 복잡하므로 간략하게 어떻게 작동되는지만 설명하겠습니다.
PS의 경우는 쿼리가 이미 database의 내부 procedure로 변환되어져 있기 때문에 Stream을 이용하여 데이터를 세팅시키는 일을 한다는 것이 Statement Batch와 틀린 점입니다. 그렇다면 Statement와 
PreparedStatement의 속도차이는 얼마나 될까요? 놀새는 이미 전부 다 해봤기 때문에 이건 여러분의 숙제로 남겨두겠습니다.

위의 내용복잡하면 몰라도 됨을 강력히 주장합니다. 당신이 JDBC Driver개발자가 아니니까요~!! ^^

자 그럼 이제 코더의 신분으로 돌아왔다고 가정을 하고, API를 이용하여 코딩이나 한번 해보죠

단순히 API를 사용하는 것이므로 거창하게 설명하고 자시고 할 필요도 없이 바로 들어갑니다. 


    
public class BatchTest {
    private Connection getConnection(){
        // DB 연결하세요
    }
    
    private void close() {
        // 연결 닫으세요
    }

    public void insertABT231Batch(){
        System.out.println(Utility.getTime() 
        + " Insert ABT231 Batch Start .. Transaction size is " + m_abt231InsertList.size());
        Connection conn = null;
        PreparedStatement pstmt = null;
        try {
            StringBuffer query = new StringBuffer();
            query.append("INSERT INTO ABT231 ");
            query.append("(customer_no, item_cd, occur_amt, reason_cd, register_ymd, register_no) ");
            query.append(" VALUES (?, ?, ?, '9', ?, ?) ");

            conn = getConnection();
            conn.setAutoCommit(false);

            pstmt = conn.prepareStatement(query.toString());
            Iterator iter = m_abt231InsertList.iterator();
            int count = 0;

            while( iter.hasNext() ) {
                m_abt231 = (Abt231) iter.next();
                pstmt.setInt(1, m_abt231.getCustomerNo());
                pstmt.setString(2, m_abt231.getItemCd());
                pstmt.setLong(3, m_abt231.getOccurAmt());
                pstmt.setString(4, s_magamCurrentTime);
                pstmt.setInt(5, Integer.parseInt(s_workCd));
                pstmt.addBatch();
                count++;
                if( (count % 10000) == 0){
                    System.out.println(count + "건 처리중");
                    pstmt.executeBatch();
                }
            }

            pstmt.executeBatch();
            conn.commit();
            System.out.println(Utility.getTime() + "] " + count + "건 입력완료");
        } catch ( Exception e) {
            e.printStackTrace();
            try{
                conn.rollback();
            }catch(Exception e2) {e2.printStackTrace();}
        } finally {
            close(pstmt);
            close(conn);
        }
    }
}