대용량 Insert, Update시 Batch처리 주의점 및 설정(Mybatis, Ibatis)

[Desc] In ORACLE

대용량 Insert, Update,시에 우리는 고민을 해봐야 합니다.

대량을 건바이건으로 Insert시에 매번 커넥션을 맺어 Insert Or Update시에는 많은 수행시간이 걸립니다.

그렇기 때문에 Batch를 이용하여 다중의 Insert, Update문을 넣어 한번에 수행하도록 하는데

이때 주의가 필요할 점이 있습니다.


[EX] 10만건을 Insert or Update시에 실행속도 In Oracle

Test프로젝트 Insert 끝나는 수행시간(stopWatch) : [173.578] - 10만건 한꺼번에 Insert시

Test프로젝트 Insert 끝나는 수행시간(stopWatch) : [161.939] - 1만건씩 나누어 10번 수행하여 Insert시

Test프로젝트 Insert 끝나는 수행시간(stopWatch) : [62.826] - 1천건씩 나누어 100번 수행하여 Insert시


만약에 Insert한건이 20만건 이상을 Insert를 하게 되면 Out Of Memory가 발생합니다.

실제로 25만건을 한꺼번에 Insert하다가 문제가 발생했습니다.

Insert, Update해야할 때는 항상 얼마나 많은 양의 데이터를 처리하는가를 생각하고

양을 알수 없다면 1천건씩 처리하는 로직을 짜는게 낫습니다.


1만건과 1천건이 왜이리 차이가 나는 것일까?

오라클에서 이야기하길 Batch의 최고의 퍼포먼스는 5~30사이일때라고 이야기합니다.

하지만 실제로 더 큰값을 주었을 때 성능향상이 있었습니다. 

그러므로 적절한 배치크기를 설정해 주어야 최상의 퍼포먼스를 얻을 수 있습니다.


[설정]

	/**
     * Mybatis 설정
     */
    private org.apache.ibatis.session.Configuration getMybatisConfig() {
        org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
        config.setCacheEnabled(false);
        config.setUseGeneratedKeys(false);
        config.setDefaultExecutorType(ExecutorType.BATCH); //이부분이 중요합니다.
        config.setMapUnderscoreToCamelCase(true);
 
        /*
         * 조회시 하나의 레코드 기준으로 데이터가 없는 컬럼에 null 설정
         */
        config.setCallSettersOnNulls(true);
        return config;
    }
 
 
//위에 방법이 아니면
    @Bean(name = "sqlSessionTemplateOrd", destroyMethod = "clearCache")
    public SqlSessionTemplate sqlSessionTemplateOrd(@Qualifier("sqlSessionFactoryOrd") SqlSessionFactory sqlSessionFactoryOrd) {
        return new SqlSessionTemplate(sqlSessionFactoryOrd, ExecutorType.BATCH); //섹션템플릿 만들때 이런식으로 배치타입을 추가해줘도 됩니다.
    }
 
 
//위에서 또 에러날 가능성이 있는 거는
커넥션을 맺을때 return new SqlSessionTemplate(sqlSessionFactoryOrd, ExecutorType.BATCH) 이부분에서
return new SqlSessionTemplate(sqlSessionFactoryOrd) 입력을 안하게 되면
Default로 ExcutorType.SIMPLE 로 연결이 되는데
나는 만약에
배치타입과 심플타입의 연결을 두개를 사용해서 select를 할때는 심플로
실제 insert 배치할때만 배치타입의 연결을 사용하고 싶다고 했을 경우
 
 
하나의 트랜잭션에 두개의 연결타입이 다른 것을 사용 할 수 없습니다.
 
 
즉
transaction  시작
     select 할때는 ExcutorType.SIMPLE 커넥션을 사용하고
     insert 할때는 ExcutorType.BATCH 커넥션을 사용한다고 하면
transaction 끝
위와 같이 사용시에는 오류가 발생합니다.
왜냐면 커넥션타입이 서로 다른것들이 하나의 트랙잭션에 묶여있기 때문입니다.
트랜잭션을 잡을때는 서로 다른 커넥션이라면 서로다른 트랙잭션 위아래로 트랙잭션을 잡아서 사용하여야 합니다.
뭐 당연한 이야기겠져?? 하지만 실수 하는 부분이라 미리 글을 남깁니다.

IBatis Batch처리

//DAO파일부분
int result = 0;
    SqlMapClient sql = SqlMapClientManager.sqlMap(Constants.DB_BKO);
 
try {
    sql.startTransaction();
    sql.startBatch();
 
    int totalSize = list.size();
    for(int i=0; i < totalSize; i++) {
        RemindDto dto = list.get(i);
        sql.insert("remind.insertPush", dto); //벌크INSERT가 아닌 방법
        if(i%1000 == 0) {
            //천건단위로
            sql.executeBatch();
            sql.startBatch();
        }
    }
 
    sql.executeBatch();
    sql.commitTransaction();
 
    result = 1;
} catch (Exception _e) {
    _e.printStackTrace();
} finally {
    try {
        SqlMapClientManager.sqlMap(Constants.DB_BKO).endTransaction();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

MyBatis에서 Batch처리는 startBatch부분이 필요없습니다. 

Datasource에 Batch_type을 Batch로 설정하게되면 자동으로해줍니다.

//service 파일 부분
List targetDivideList = new ArrayList();
int insertCnt = 0;
for(int i=0, totalSize = targetList.size(); i < totalSize; i++) {
	RemindDto dto = targetList.get(i);
	targetDivideList.add(dto);
	
	if(insertCnt == 999 || totalSize==(i+1)) {
		remindDao.insertPush(targetDivideList);
		targetDivideList = new ArrayList<>();
		insertCnt = 0;
	} else {
		insertCnt++;
	}
}
 
 
//DAO 파일 부분
@Override
public int insertPush(List list) throws Exception {
    if(list == null || list.size() == 0) {
        throw new NullPointerException("list가 없습니다");
    }
    sqlSessionTmp.insert("remind.insertPush", list); //list에 주목해주세요 벌크INSERT방법
    return 1;
}
 
 
//XML 부분 list를 받아서 처리 xml에서 foreach문으로 벌크insert시 훨씬더 빠르게 할수 있습니다.

    INSERT ALL  /*  remind.insertTest   */
    
        INTO TEST_H (SEND_DT,CREATOR_ID) VALUES (SYSDATE,'junhyun')
    
    SELECT * FROM DUAL


마지막으로 나는 더 빠르게 하고 싶다는 분들은

IBatis나 Mybatis를 이용하는 방법이 아닌

addBatch에서 PrepareStatement를 이용하는 방법이 있습니다.

EX) 예를 찾아 넣었습니다.
public class TestAddBatch {
  
    public static void main(String[] args) {
        // TODO Auto-generated method stub
          
          
        Connection con = null;
        PreparedStatement pstmt = null ;
          
        String sql = "Insert Into TB_test(uid, name, age) Values(?, ?, ?)" ;
          
          
        try{
            Class.forName("com.mysql.jdbc.Driver");
            con = DriverManager.getConnection(" jdbc:mysql://127.0.0.1:3306/TEST_DB", "test_user", "12345");
              
              
            pstmt = con.prepareStatement(sql) ;
              
              
            for(int i=0; i < 100000; i++){
                int uid = 10000 + i ;
                String name = "홍길동_" + Integer.toString(i) ;
                int age = i ;
              
                pstmt.setInt(1, uid);
                pstmt.setString(2, name) ;
                pstmt.setInt(3, age);
                  
                // addBatch에 담기
                pstmt.addBatch();
                  
                // 파라미터 Clear
                pstmt.clearParameters() ;
                  
                  
                // OutOfMemory를 고려하여 만건 단위로 커밋
                if( (i % 10000) == 0){
                      
                    // Batch 실행
                    pstmt.executeBatch() ;
                      
                    // Batch 초기화
                    pstmt.clearBatch();
                      
                    // 커밋
                    con.commit() ;
                      
                }
            }
              
              
            // 커밋되지 못한 나머지 구문에 대하여 커밋
            pstmt.executeBatch() ;
            con.commit() ;
              
        }catch(Exception e){
            e.printStackTrace();
              
            try {
                con.rollback() ;
            } catch (SQLException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
              
        }finally{
            if (pstmt != null) try {pstmt.close();pstmt = null;} catch(SQLException ex){}
            if (con != null) try {con.close();con = null;} catch(SQLException ex){}
        }
  
    }
}



블로그 이미지

pstree

pstree.. process...

,