saveAll을 썼는데 insert가 1,000번 나간 이유

정산 배치에서 수천 건을 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으로 직접 넣는 것도 선택지입니다.