정산 배치에서 수천 건을 saveAll로 한 번에 저장하도록 짰습니다. 코드는 한 줄이라 당연히 묶어서 나갈 줄 알았는데, 쿼리 로그를 켜 보니 insert가 건수만큼 개별로 나가고 있었습니다. 1천 건이면 1천 번이었죠.
List<Settlement> rows = build(...); // 1,000건
settlementRepository.saveAll(rows); // insert가 1,000번
saveAll은 배치를 보장하지 않는다
saveAll은 그냥 반복해서 저장을 호출하는 편의 메서드일 뿐, 그 자체로 JDBC 배치로 묶어 주지 않습니다. 게다가 결정적인 문제가 하나 더 있었는데, 엔티티의 기본키 전략이 IDENTITY였다는 점입니다. IDENTITY는 INSERT를 실제로 실행해야 DB가 매긴 ID를 알 수 있기 때문에, 하이버네이트가 INSERT를 모아 뒀다가 한꺼번에 보내는 배치 자체를 꺼 버립니다.
전략을 바꾸고 배치를 켰다
| 설정 | 효과 |
|---|---|
| 키 전략 SEQUENCE + allocationSize | 배치 가능해짐 |
| hibernate.jdbc.batch_size=500 | INSERT 묶음 전송 |
| rewriteBatchedStatements=true | 드라이버가 한 문장으로 재작성 |
jdbc:mysql://.../db?rewriteBatchedStatements=true
spring:
jpa:
properties:
hibernate:
jdbc.batch_size: 500
결과
키 전략을 배치와 호환되는 방식으로 바꾸고 위 옵션을 켜니, 1천 번 나가던 INSERT가 몇 방으로 줄었습니다. 정산 배치 시간이 분 단위에서 초 단위로 떨어졌고요. 대량 저장이 느릴 땐 saveAll을 의심하기 전에, 키 생성 전략과 배치 옵션부터 확인하는 습관이 생겼습니다. 정말 큰 벌크라면 JdbcTemplate으로 직접 넣는 것도 선택지입니다.