목록으로

MySQL 배치 INSERT가 느린 이유: rewriteBatchedStatements

13

MySQL 배치 INSERT가 느린 이유: rewriteBatchedStatements

rewriteBatchedStatements는 MySQL JDBC 드라이버 옵션으로, 여러 개의 INSERT 문을 하나의 멀티 로우 INSERT 문으로 합쳐서 보내주는 기능이다.

2026-02-11


발단

알림센터에 알림을 벌크로 저장하는 기능이 있다. 한 번에 수천~수만 건을 INSERT 한다.

그런데 속도가 기대보다 많이 느렸다. JPA의 saveAll()로 배치 INSERT를 하고 있었고, Hibernate(JPA 표준을 구현한 가장 대표적인 ORM 프레임워크) 배치 설정도 해놨는데.

spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 5000

JPA 배치가 뭔데?

saveAll()에 5,000건을 넘기면, Hibernate는 5,000개의 INSERT를 모아서 한 번에 보낸다... 라고 생각하기 쉽다.

실제로는 아니다.

Hibernate 배치 (batch_size: 5000)
  → JDBC 드라이버에 5,000개의 addBatch() 호출
  → executeBatch() 실행

여기까지는 맞다. 그런데 MySQL JDBC 드라이버(자바 애플리케이션이 MySQL 데이터베이스와 통신할 수 있게 해주는 라이브러리)가 이걸 네트워크로 보낼 때:

기본 동작 (rewriteBatchedStatements=false):
  INSERT INTO t VALUES (1, 'a');
  INSERT INTO t VALUES (2, 'b');
  INSERT INTO t VALUES (3, 'c');
  ... 5,000번 반복

→ 5,000개의 개별 INSERT가 네트워크를 탄다!

Hibernate가 아무리 배치를 만들어도, MySQL 드라이버가 개별 INSERT로 풀어서 보내버리는 것이다.


rewriteBatchedStatements=true

JDBC URL에 이 옵션을 추가하면:

rewriteBatchedStatements=true:
  INSERT INTO t VALUES (1, 'a'), (2, 'b'), (3, 'c'), ...;

→ 하나의 멀티 로우 INSERT로 합쳐서 전송!
# Before
url: jdbc:mysql://host:3306/mydb

# After
url: jdbc:mysql://host:3306/mydb?rewriteBatchedStatements=true

이것만으로 벌크 INSERT 속도가 수십 배 빨라질 수 있다.


왜 기본값이 false인데?

MySQL 공식 문서에 따르면:

  • rewriteBatchedStatements=true는 멀티 로우 INSERT로 변환할 때 SQL 구문이 변경된다
  • ON DUPLICATE KEY UPDATE 같은 구문에서 VALUES() 함수의 동작이 달라질 수 있다
  • 호환성 이슈로 기본 꺼짐

대부분의 일반적인 INSERT에서는 켜도 문제가 없다.


같은 날 수정한 Spring Boot 초기화 이슈들

배치 성능을 수정하면서 Spring Boot 경고도 같이 정리했다.

@PostConstruct vs @EventListener(ApplicationReadyEvent)

→ 빈(Bean)은 Spring이 관리하는 객체를 말한다. Spring은 애플리케이션 시작 시 필요한 객체들을 자동으로 생성하고 관리하는데, 이 객체들이 빈이다.

// Before — @PostConstruct
@PostConstruct
public void init() {
    // Kafka가 아직 초기화 안 됐을 수 있음!
    checkKafkaTopics();
}

// After — ApplicationReadyEvent
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
    // 모든 빈 초기화 완료 후 실행
    checkKafkaTopics();
}

@PostConstruct는 해당 빈이 생성된 직후 실행된다. 다른 빈이 아직 초기화가 안 됐을 수 있다. ApplicationReadyEvent는 모든 빈 초기화가 끝난 후 발생한다.

@Configuration + @Bean = static이 안전

@Configuration은 이 클래스가 Spring 설정 파일 역할을 한다는 표시이고, @Bean은 이 메서드가 반환하는 객체를 Spring이 관리하라는 표시다.

@Configuration
public class MyConfig {
    // Before — 인스턴스 메서드
    @Bean
    public Validator validator() { ... }

    // After — static 메서드 (Spring Boot 3.x 권장)
    @Bean
    static Validator validator() { ... }
}

@Bean 메서드가 non-static이면 설정 클래스의 전체 라이프사이클(객체가 생성되고 사용되고 소멸되는 전체 과정)에 의존하게 된다. static으로 만들면 설정 클래스 인스턴스 없이도 빈을 생성할 수 있어서 초기화 순서 문제를 피할 수 있다.


배운 것

  1. MySQL + JPA 배치를 쓴다면 rewriteBatchedStatements=true는 거의 필수다. 안 켜면 배치가 의미 없다.
  2. Hibernate batch_size만으로는 충분하지 않다. 드라이버 레벨에서도 배치를 지원해야 한다.
  3. @PostConstruct 대신 @EventListener(ApplicationReadyEvent.class)가 안전하다. 다른 빈에 의존하는 초기화 로직이라면 특히.
  4. Spring Boot 3.x에서 @Bean 메서드는 static으로 만들 수 있다면 만들자.
MySQL 배치 INSERT가 느린 이유: rewriteBatchedStatements | KYUDORI