Framework/Spring

[Spring Boot]대량 데이터 저장하기

SHXL2 2024. 10. 17. 14:24
반응형

최근 약 3만건의 데이터를 저장하는 일이 있었습니다. 이번 포스팅에서는 MyBatis로 대량 데이터 저장할 때 효과적인 방법에 대해 알아보겠습니다.

 

테스트 환경
  • Spring Boot 2.7
  • JDK11
  • MyBatis
  • Oracle

대량의 데이터를 저장해야할 때 단건씩 저장하면 매우 많은 시간을 소요하게 됩니다. 때문에 보통 Bulk Insert를 통해 저장합니다.

Oracle에서 Bulk Insert시에 사용할 수 있는 쿼리는 2가지로 볼 수 있습니다.

 

INSERT ALL

INSERT ALL은 여러 테이블에 동시에 데이터를 입력할 때 사용할 수 있는 구문입니다.

INSERT ALL 
    INTO [TABLE] (col1, col2) VALUES ('value1','value2')
    INTO [TABLE] (col1, col2) VALUES ('value1','value2')
    INTO [TABLE] (col1, col2) VALUES ('value1','value2')
    ...
SELECT * FROM DUAL;

 

UNION ALL(SELECT - INSERT)

여러 개의 select문을 사용해 한번에 insert 하는 구문입니다.

INSERT INTO (col1, col2)
SELECT value1, value2 FROM DUAL UNION ALL
SELECT value1, value2 FROM DUAL UNION ALL
...
SELECT value1, value2 FROM DUAL
;

 

 

본 예제에서는 UNION ALL을 활용해서 MyBatis에서는 foreach 문을 만들어 데이터를 입력해보겠습니다. 이 때 MyBatis에서 제공하는 Batch Insert와 기본 Insert의 실행 속도를 비교해보겠습니다.

<insert id="insertDummy" parameterType="List">
    <![CDATA[
        INSERT INTO DUMMY_TABLE (
              COL1
            , COL2
            , COL3
            , COL4
        )
    ]]>
    <foreach collection="list" item="item" separator="FROM DUAL UNION ALL" close="FROM DUAL ">
        SELECT
              #{item.col1}
            , #{item.col2}
            , #{item.col3}
            , #{item.col4}
    </foreach>
</insert>

 

 

입력 데이터는 1만건이며 테스트 코드는 다음과 같습니다.

  • insertDummyWithForeach : Insert
  • insertDummyWithBatch : Batch Insert
@Transactional
@Override
public void insertDummyWithForeach(List<dummyDto> dummyDtoList) {
    int batchSize = 1000;
    long start = System.currentTimeMillis();
    List<DummyDto> insertList = new ArrayList<>();

    for (int i = 0; i < dummyDtoList.size(); i += batchSize) {
        insertList = dummyDtoList.subList(i,
            Math.min(i + batchSize, dummyDtoList.size()));
        testDao.insertDummy(insertList);
    }

    long executionTime = System.currentTimeMillis() - start;
    log.info("Foreach insert execution : {} ms ", executionTime);
}

@Override
public void insertDummyWithSqlSession(List<dummyDto> dummyDtoList) {
    int batchSize = 1000;
    long start = System.currentTimeMillis();
    List<DummyDto> insertList = new ArrayList<>();
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

    try {
        TestDao testDao = sqlSession.getMapper(TestDao.class);

        for (int i = 0; i < dummyDtoList.size(); i += batchSize) {
            insertList = dummyDtoList.subList(i,
                Math.min(i + batchSize, dummyDtoList.size()));
            testDao.insertHantDummy(insertList);
        }

        sqlSession.flushStatements();
        sqlSession.commit();
        long executionTime = System.currentTimeMillis() - start;
        log.info("Batch insert execution : {} ms ", executionTime);
    } catch (Exception e) {
        log.error(e.getMessage());
        sqlSession.rollback();
        throw new RuntimeException(e);
    } finally {
        sqlSession.close();
    }
}

 

두 메소드는 동일하게 리스트 1000건씩 저장하게 됩니다.

결과를 먼저 확인해보겠습니다.

- insertDummyWithForeach 

2024-10-17 13:55:43.733  INFO 53140 --- [nio-8080-exec-9] c.a.d.d.c.service.impl.TestServiceImpl  : Foreach insert execution : 17527 ms

 

- insertDummyWithBatch 

2024-10-17 13:58:00.709  INFO 19396 --- [nio-8080-exec-4] c.a.d.d.c.service.impl.TestServiceImpl  : Batch insert execution : 1850 ms

 

Batch를 사용하지 않은 경우 시간은 17초정도가 소요됐으며, Batch를 사용한 경우 약 1.8초정도의 시간이 소요된 것을 확인할 수 있습니다.

SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

try {
    TestDao testDao = sqlSession.getMapper(TestDao.class);

    for (int i = 0; i < dummyDtoList.size(); i += batchSize) {
        insertList = dummyDtoList.subList(i,
            Math.min(i + batchSize, dummyDtoList.size()));
        testDao.insertHantDummy(insertList);
    }

    sqlSession.flushStatements();
    sqlSession.commit();
    long executionTime = System.currentTimeMillis() - start;
    log.info("Batch insert execution : {} ms ", executionTime);
} catch (Exception e) {
    log.error(e.getMessage());
    sqlSession.rollback();
    throw new RuntimeException(e);
} finally {
    sqlSession.close();
}

 

  • SqlSession : MyBatis에서 데이터베이스 작업을 수행하기 위해 사용되는 SqlSession 객체입니다. ExecutorType.BATCH 옵션을 사용하면 배치 모드로 세션이 활성화 됩니다. 배치 모드를 사용하면 다수의 SQL 문을 하나의 배치로 처리할 수 있어 성능 향상에 기여합니다. SqlSession을 통해 트랜잭션을 제어하기 때문에 @Transactional 어노테이션은 제외합니다.
  • sqlSession.flushStatements() : 배치 모드에서는 SQL 문이 바로 실행되지 않고, 일정 수량이 모일 때 한 번에 실행됩니다.  배치로 쌓인 SQL 문을 실제로 실행합니다.
  • sqlSession.commit() : 트랜잭션을 커밋하여 데이터베이스에 영구적으로 적용합니다.

 

이와같이 Batch를 충분히 활용해 트랜잭션 관리, 예외 처리, 리소스 정리를 신경쓴다면 대량 데이터를 안정적으로 처리할 수 있는 구조를 만들 수 있습니다.


+ 피드백은 언제나 환영입니다 :)

 

 

반응형