미니옵빠의 code stubs
Java 배치 개발 시 iBatis queryWithRowHandler 사용하기 본문
대부분 배치들이 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 를 사용하면 되네요..