@Entity
public class Course extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
private String address;
private Double latitude;
private Double longitude;
}
MySQL에서 IDENTITY는 AUTO_INCREMENT 기반입니다. 즉, INSERT가 실제로 실행된 뒤에야 ID를 알 수 있습니다.
Hibernate는 엔티티를 영속성 컨텍스트에 넣으려면 식별자가 필요합니다. 그런데 IDENTITY 전략에서는 INSERT 전까지 ID가 없습니다. 결국 Hibernate는 쓰기 지연 저장소에 모아두지 못하고, persist() 시점에 바로 INSERT를 실행해 ID를 받아와야 합니다.
즉, 이 문제는 “batch 설정이 적용되지 않는다”가 아니라, IDENTITY 전략에서는 batch를 적용할 전제가 성립하지 않는다는 데 있었습니다.
그래서 먼저 로그로 확인했습니다
가설을 세운 뒤에는 실제로 어떤 SQL이 실행되는지 확인해야 했습니다. 10개 데이터만 저장하면서 로그를 봤습니다.
java
@Test
void testSmallBatchForLogAnalysis() {
List courses = generateTestCourses(10);
courseRepository.saveAll(courses);
}
결과는 예상과 달랐습니다.
batch_size: 20으로 설정했는데도 INSERT가 묶이지 않았습니다.
saveAll() 호출 시점부터 INSERT가 바로 실행됐습니다.
트랜잭션 커밋 직전까지 모였다가 한 번에 나가는 흐름이 아니었습니다.
📸 10개 데이터 INSERT 로그
이 시점부터 문제는 명확해졌습니다. JPA가 느리다는 뜻이 아니라, 지금 선택한 ID 전략에서는 Hibernate batch가 동작할 수 없는 구조라는 뜻이었습니다.
수치로 보면 더 분명했습니다
그다음에는 실제로 저장 시간이 얼마나 차이 나는지 확인했습니다. 100개, 500개 데이터를 각각 저장하면서 시간을 측정했습니다.
java
@Test
void testLargeBatchPerformance() {
List courses = generateTestCourses(500);
long startTime = System.currentTimeMillis();
courseRepository.saveAll(courses);
long endTime = System.currentTimeMillis();
System.out.println("총 소요 시간: " + (endTime - startTime) + "ms");
}
당시 측정 결과는 다음과 같았습니다.
100개 저장: 약 423ms
500개 저장: 약 2702ms
📸 500개 저장 테스트 결과
중요한 건 절대값보다 패턴입니다. 데이터가 늘어날수록 INSERT가 여전히 한 건씩 나가고 있었고, 따라서 애플리케이션이 기대한 배치 처리 이점을 얻지 못하고 있었습니다.
그래서 JPA를 우회했습니다
이 문제를 해결하려면 Hibernate가 해결해 주길 기대할 수 없었습니다. IDENTITY 전략을 유지해야 하는 MySQL 환경에서는, 대량 INSERT 구간만 영속성 컨텍스트 밖으로 빼는 편이 더 현실적이었습니다.