본문 바로가기

Programming/Database

JPA 대량 INSERT 성능 개선: Hibernate JDBC 배치와 flush·clear 설정

추천캐릭터 2026. 8. 4. 08:00
728x90

JPA 대량 INSERT 성능 개선: Hibernate JDBC 배치와 flush·clear 설정

메타 설명: Spring Boot와 MariaDB에서 JPA 대량 INSERT를 JDBC 배치로 묶고, IDENTITY 제약과 영속성 컨텍스트 메모리 문제를 해결하는 설정·코드·검증 방법을 설명합니다.

CSV에서 회원 10만 건을 가져오거나 야간 집계 결과를 저장할 때 repository.saveAll()만 호출하면 충분하다고 생각하기 쉽습니다. 하지만 Hibernate의 JDBC 배치가 꺼져 있거나 기본 키가 IDENTITY라면 INSERT가 한 건씩 전송될 수 있습니다. 한 트랜잭션에서 엔티티를 계속 관리하면 1차 캐시도 커집니다.

대량 저장 성능은 세 가지를 함께 봐야 합니다.

  1. JDBC 드라이버에 INSERT를 묶어 전달하는가
  2. 기본 키 생성 방식이 배치를 막지 않는가
  3. 영속성 컨텍스트가 엔티티를 무한히 보관하지 않는가

saveAll과 JDBC 배치는 같은 기능이 아니다

saveAll()은 여러 엔티티를 반복 저장하는 API일 뿐 네트워크 왕복을 자동으로 한 번으로 만들지 않습니다. SQL 묶음은 Hibernate와 JDBC 드라이버가 담당합니다.

방식 적합한 상황 주의점
Hibernate 배치 JPA 모델을 유지하는 수천~수만 건 IDENTITY 제한, 1차 캐시 관리
JdbcTemplate.batchUpdate() SQL 제어가 중요한 대량 적재 생명주기를 직접 관리

Hibernate 배치 설정

spring:
  datasource:
    url: jdbc:mariadb://localhost:3306/app?rewriteBatchedStatements=true
    username: app
    password: ${DB_PASSWORD}
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true
        generate_statistics: true # 검증 환경에서만 사용

spring.jpa.properties.* 아래에는 Hibernate 속성 이름을 정확히 씁니다. order_inserts는 배치 기회를 늘리지만 정렬 비용이 있으므로 전후를 측정합니다.

rewriteBatchedStatements=true는 Connector/J에 INSERT 배치 재작성을 요청합니다. 효과는 로그와 처리 시간으로 확인합니다.

AUTO_INCREMENT가 배치를 막는 이유

다음과 같은 엔티티는 MariaDB의 AUTO_INCREMENT를 흔히 사용합니다.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

Hibernate는 INSERT 직후 생성 ID를 알아야 합니다. 공식 문서에 따르면 identity 식별자 생성기를 쓰면 INSERT JDBC 배치가 비활성화됩니다. SQL이 한 줄씩 실행된다면 ID 전략부터 확인합니다.

MariaDB는 시퀀스를 지원하므로 신규 테이블이라면 다음 대안을 검토할 수 있습니다.

CREATE SEQUENCE import_item_seq
    START WITH 1
    INCREMENT BY 50
    CACHE 100;
@Entity
@Table(name = "import_item")
public class ImportItem {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "import_item_seq")
    @SequenceGenerator(
        name = "import_item_seq",
        sequenceName = "import_item_seq",
        allocationSize = 50
    )
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    protected ImportItem() {
    }

    public ImportItem(String name) {
        this.name = name;
    }
}

DB의 INCREMENT BY와 JPA의 allocationSize를 맞춥니다. 기존 AUTO_INCREMENT 전략 변경은 복제와 외부 참조에 영향을 주므로, 기존 스키마에는 JdbcTemplate.batchUpdate()가 더 안전할 수 있습니다.

flush와 clear로 1차 캐시 제한하기

@Service
public class ImportService {

    private static final int BATCH_SIZE = 50;

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void importNames(List<String> names) {
        for (int i = 0; i < names.size(); i++) {
            entityManager.persist(new ImportItem(names.get(i)));

            if ((i + 1) % BATCH_SIZE == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }

        entityManager.flush();
        entityManager.clear();
    }
}

flush()는 SQL을 DB로 보내고 clear()는 관리 엔티티를 분리해 1차 캐시 크기를 제한합니다.

clear() 뒤 엔티티는 준영속 상태이므로 변경 감지를 기대하면 안 됩니다. 재시작이 중요하면 입력을 청크로 나누고 청크별 트랜잭션 경계를 둡니다.

실제로 묶였는지 검증하기

설정 파일만 보고 적용됐다고 판단하지 않습니다.

  1. 동일한 데이터 1천~1만 건으로 배치 전후 처리 시간을 측정합니다.
  2. Hibernate 통계의 prepared statement 수와 entity insert 수를 비교합니다.
  3. MariaDB Connector/J 로그 또는 테스트용 프록시 드라이버로 executeBatch 호출을 확인합니다.
  4. ID 전략을 IDENTITYSEQUENCE로 바꿔 차이를 비교합니다.
  5. 테스트 종료 뒤 레코드 수, 중복 키, 실패 청크의 롤백 범위를 검증합니다.

검증이 끝나면 성능에 영향을 주는 통계와 SQL 상세 로그를 끕니다.

흔한 실수

  • saveAll()이 자동으로 JDBC 배치를 만든다고 가정한다.
  • GenerationType.IDENTITY를 유지한 채 batch_size 숫자만 높인다.
  • 10만 건을 한 번에 persist하고 마지막에만 flush해 1차 캐시를 키운다.
  • clear() 뒤 엔티티에 변경 감지가 계속 적용된다고 생각한다.
  • 배치 크기를 무조건 크게 잡아 패킷 크기와 잠금 시간을 늘린다.
  • 처리 시간만 보고 부분 실패의 재시작 전략과 중복 방지를 빼먹는다.

핵심 요약

  • saveAll()과 JDBC 배치는 서로 다른 계층의 기능이다.
  • Hibernate 배치는 hibernate.jdbc.batch_size를 정확한 이름으로 설정한다.
  • IDENTITY 기본 키는 Hibernate INSERT 배치를 제한하므로 ID 전략을 먼저 확인한다.
  • flush()clear()를 배치 간격으로 호출해 1차 캐시 크기를 제한한다.
  • 배치 크기, ID 전략, 트랜잭션 청크는 반드시 같은 데이터로 전후 측정한다.

함께 읽기

최종 확인: 2026-08-03

공식 출처:

728x90