미니옵빠의 code stubs

Java 배치 개발 시 iBatis queryWithRowHandler 사용하기 본문

Framework/iBatis

Java 배치 개발 시 iBatis queryWithRowHandler 사용하기

미니옵빠 2014. 3. 18. 10:20

대부분 배치들이 Java 로 DB 에서 데이터를 가지고 와서 핸들링하는 것이 일반적인 패턴인데요,

대용량 RecordSet 을 가지고 올 때 OOM 이슈를 해결하기 위해서는 iBatis 의 queryWithRowHandler 를 사용하면 됩니다.

 

본론으로 들어가서,

일반적으로 아래의 방식으로 배치를 개발할 껀데요

 

1. iBatis 에서 데이터 List Select 후 DAO 에 ArrayList 로 받아 Loop 돌면서 처리

2. Spring Batch 로 Step 별로 Read > Processor > Writer 

3. Php 등 다른 언어로 개발
 

 

1번의 경우 주로 이슈가 되는 부분이구요 (JDBC 에 날로 Connection 맺어 statement 만들어 사용하는 경우도 동일) 

엄청 많은 양의 레코드 (데이터)를 Select 하면 OOM 이 뜹니다. 메모리에 일단 전부 담고 시작하니까 당연히 OOM이 날 수 밖에 없어요.

 

요렇때 iBatis 의 queryWithRowHandler 를 쓰면 레코드를 한 건씩 반환 받을 수 있고, 각 레코드별로 처리가 가능합니다.

 

아래와 같이 RowHandler 의 구현체를 하나 만들어 레코드 한 건별로 처리할 동작을 구현하고,

SqlMapClientTemplate.queryWithRowHandler() 에 넘겨주면 처리가 되는 형태입니다.

 

예시)

[DAO]

public void selectLaunchingContentsList(RowHandler rowHandler) {

sqlMapClientTemplate.queryWithRowHandler("쿼리 ID", rowHandler);

}

 

[BO]

/**

 * 런칭된 콘텐츠 리스트 조회

 * 

 * @return

 */

public void calcStatsTagUse() {

ContentsModelRowHandlerCallback callback = new ContentsModelRowHandlerCallback();

contentsDAO.selectLaunchingContentsList(callback);

}

/**

 * 콘텐츠 모델 집계를 실제로 실행할 콜백 인스턴스

 * 

 * @author tod2

 */

public class ContentsModelRowHandlerCallback implements RowHandler {

    @Override

    public void handleRow(Object arg0) {

     ContentsModel item = (ContentsModel) arg0;

 

     // TODO

     System.out.println(item);

    }

}

 

 

여기서 주의할 것이 있는데요, ibatis의 SQL MAP 에 아래와 같이 fetchSize 를 추가해 주어야 합니다.

 

 

 <select id="selectLaunchingContentsList" resultClass="com.naver.contents.model.ContentsModel" fetchSize="-2147483648">


이걸 안하면, Mysql JDBC에서 자체 ResultSet 을 만들어 메모리에 올리는 형태이기 때문에 똑같이 OOM이 나구요

 

iBatis 의 동작을 예로 들면...

 

Mysql JDBC > ResultSet 생성 (Memory 적제) > iBatis 로 전달 > iBatis 에서 ArrayList로 Value copy? Ref 전달?  후 return  

 

이런 과정이라, 저 ResultSet 부분이 실제 OOM 을 유발하게 됩니다

 


참고로 저 fetchSize 값은 Integer.MIN_VALUE 입니다.

 

http://dev.mysql.com/doc/refman/5.0/es/connector-j-reference-implementation-notes.html 

여기를 보시면 ResultSet 부분에 저렇게 세팅해야 row-by-row 로 retrieve 한다고 되어 있어요. 



stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
              java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

 

The combination of a forward-only, read-only result set, with a fetch size of Integer.MIN_VALUE serves as a signal to the driver to stream result sets row-by-row. After this any result sets created with the statement will be retrieved row-by-row.



그리고 이 동작 중에는 Lock이 걸린다고 합니다. 대부분 레코드 단위로 lock 이 걸릴꺼니까 유난히 무거운 Insert 작업하고만 겹치지 않으면 크게 문제는 없어 보이네요. 요건 좀 더 해보면 알 듯..


 

그리고 MYSQL + rowHandler 사용 시 캐시 버그가 있습니다.

 

http://www.rcy.co.kr/xeb/index.php?document_srl=2342&mid=study&sort_index=readed_count&order_type=desc

와 같이 캐시 버그가 있다고 합니다.

 

해결하려면

 

SELECT SQL_NO_CACHE * from Table...

 

와 같이 쿼리 캐시 사용 중지 syntax 를 사용하면 되네요..