[Spring Boot]대량 데이터 저장하기
최근 약 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를 충분히 활용해 트랜잭션 관리, 예외 처리, 리소스 정리를 신경쓴다면 대량 데이터를 안정적으로 처리할 수 있는 구조를 만들 수 있습니다.
+ 피드백은 언제나 환영입니다 :)