JPA 대량 INSERT 성능 개선: Hibernate JDBC 배치와 flush·clear 설정
메타 설명: Spring Boot와 MariaDB에서 JPA 대량 INSERT를 JDBC 배치로 묶고, IDENTITY 제약과 영속성 컨텍스트 메모리 문제를 해결하는 설정·코드·검증 방법을 설명합니다.
CSV에서 회원 10만 건을 가져오거나 야간 집계 결과를 저장할 때 repository.saveAll()만 호출하면 충분하다고 생각하기 쉽습니다. 하지만 Hibernate의 JDBC 배치가 꺼져 있거나 기본 키가 IDENTITY라면 INSERT가 한 건씩 전송될 수 있습니다. 한 트랜잭션에서 엔티티를 계속 관리하면 1차 캐시도 커집니다.
대량 저장 성능은 세 가지를 함께 봐야 합니다.
- JDBC 드라이버에 INSERT를 묶어 전달하는가
- 기본 키 생성 방식이 배치를 막지 않는가
- 영속성 컨텍스트가 엔티티를 무한히 보관하지 않는가
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만 건으로 배치 전후 처리 시간을 측정합니다.
- Hibernate 통계의 prepared statement 수와 entity insert 수를 비교합니다.
- MariaDB Connector/J 로그 또는 테스트용 프록시 드라이버로
executeBatch호출을 확인합니다. - ID 전략을
IDENTITY와SEQUENCE로 바꿔 차이를 비교합니다. - 테스트 종료 뒤 레코드 수, 중복 키, 실패 청크의 롤백 범위를 검증합니다.
검증이 끝나면 성능에 영향을 주는 통계와 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 전략, 트랜잭션 청크는 반드시 같은 데이터로 전후 측정한다.
함께 읽기
- Spring Boot 4.1 + Testcontainers로 MariaDB 통합 테스트하기 — 실제 MariaDB에서 배치 동작을 재현
- Spring Boot JPA N+1 문제 해결하기 — 조회 쿼리 수와 저장 배치의 차이를 함께 이해
- Flyway로 Spring Boot DB 마이그레이션하기 — 시퀀스와 테이블 변경을 버전 관리
- HikariCP 커넥션 풀 설정 — 긴 배치 트랜잭션의 커넥션 점유 영향 확인
- MariaDB 복합 인덱스로 목록 조회 개선하기 — INSERT 성능과 인덱스 유지 비용의 트레이드오프
최종 확인: 2026-08-03
공식 출처:
'Programming > Database' 카테고리의 다른 글
| MariaDB 트랜잭션 격리수준과 데드락 원인 진단법 (0) | 2026.08.07 |
|---|---|
| Spring Boot 4.1 + Testcontainers로 MariaDB 통합 테스트하기: H2를 운영 DB처럼 믿지 말아야 하는 이유 (0) | 2026.08.01 |
| OFFSET 페이지네이션이 느려지는 이유: MariaDB 커서 방식과 Spring Data JPA 구현 (0) | 2026.07.29 |
| 낙관적 락 vs 비관적 락, 재고 차감에서 뭘 써야 하나 (0) | 2026.07.23 |
| 인덱스를 타지 않는 쿼리, 원인과 점검 (0) | 2026.07.16 |