대량 데이터 write(일괄·엑셀·통계) 시 JPA가 적합하지 않은 이유와, JDBC batchUpdate로 bulk insert 성능을 끌어올린 경험을 다룬다. 특히 1:N 연관관계가 있어 외래키 설정이 필요한 경우까지, 애플리케이션 연산으로 PK를 추론해 bulk insert 2번으로 끝내는 방법을 정리한다.
핵심 포인트- 성능 튜닝은 Read·Process·Write 중 병목을 먼저 특정하는 게 첫 단계이며, 이 글은 Write에 집중한다.
- JPA saveAll()은 이름과 달리 건건이 insert가 나간다: bulk insert는 각 엔티티 ID를 알 수 없어 PK 매핑을 위해 row별 insert가 불가피하다.
- JDBC의 batchUpdate로 다중 VALUES insert를 만들어 DB I/O를 줄이는 게 핵심이다.
- member 1만 건 + article 약 3만 건 테스트에서 JPA 78초가 JDBC 1.7초로 단축됐다.
상세 정리- 문제 인식: 장애는 자원을 꾸준히 쓸 때보다 평소 10 쓰다가 1분간 급증해 50 쓸 때 자주 발생하는데, bulk 처리가 리소스 급증의 전형이다.
- ORM 본질: ORM은 RDB와 객체 간 변환 테크닉으로 개발 편의성이 목적이지 대용량 처리 도구가 아니다.
- saveAll 한계: INSERT 후 엔티티에 ID가 매핑돼야 하는데 bulk insert는 모든 row의 ID를 알 수 없어, 결국 row별 insert 쿼리가 나간다. 기본키 전략마다 이유는 달라도 bulk 불가는 동일하다.
- 1차 개선: JdbcTemplate.batchUpdate와 BatchPreparedStatementSetter로 값만 바인딩해 한 번에 다중 VALUES insert를 실행한다.
- 1:N 난관: member-article처럼 FK가 필요하면 bulk insert 후 last_insert_id()가 첫 row PK만 주므로 각 자식의 FK를 못 채워 결국 부모 단위로 쪼개 insert하게 돼 성능 하락·DB 부하를 부른다.
- PK 추론 트릭: auto increment PK는 bulk insert 시 1씩 순차 증가가 보장되므로 첫 PK가 57이면 다음은 58·59로 계산할 수 있다.
- 구현 흐름: memberList를 batchUpdate → last_insert_id 조회 → firstPK+i로 각 member PK 설정 → FK가 채워진 articleList를 flatMap으로 생성 → article batchUpdate로, bulk insert 2번으로 FK 포함 데이터를 적재한다.
- 트레이드오프: 속도·DB 부하는 대폭 개선되나, FK 구조가 복잡할수록 애플리케이션 로직이 복잡해지고 JPA 영속성과 다르게 동작해 혼용 시 영속성 관리에 주의해야 한다.
- 결론: JPA(추상화·편의, 성능 trade off)와 JDBC(성능, 복잡도·테스트 부담)는 장단이 명확해 상황에 맞게 선택하는 게 개발자의 몫이다.
왜 읽나대량 insert 성능을 고민하는 Spring 백엔드 개발자에게 JPA saveAll의 함정과 JDBC batchUpdate + PK 추론 패턴을 알려주는 실전 가이드.