피드로 돌아가기
컬리 기술블로그Backend
원문 읽기
BULK 처리 Write에 집중해서 개선해보기
Kurly 백엔드팀이 BULK Write에서 JPA의 saveAll()을 JDBC batchUpdate로 교체하고 Auto Increment PK 연산을 활용해 10,000건 Member + 30,000건 Article 삽입 시간을 78초에서 1.7초로 단축
AI 요약
Context
대량 데이터를 처리하는 BULK 작업 중 Write 단계에서 성능 저하가 발생했다. JPA의 saveAll() 메서드는 메서드명과 달리 내부적으로 Entity의 ID 맵핑을 위해 개별 INSERT 쿼리를 건건이 실행하므로, 10만 건 규모의 데이터 삽입 시 DB 부하와 애플리케이션 성능 문제를 야기했다.
Technical Solution
- JPA saveAll() 제거 후 JDBC batchUpdate 도입: INSERT INTO ... VALUES (...), (...), (...) 형태의 BULK INSERT 쿼리 실행으로 DB I/O 횟수 감소
- Auto Increment PK 연산 기반 외래키 설정 전략 적용: bulk insert 완료 후 last_insert_id()로 첫 번째 PK를 조회한 뒤, PK가 1씩 증가한다는 특성을 이용해 삽입된 모든 Row의 ID를 애플리케이션에서 연산으로 추론
- 부모-자식 테이블 관계 처리 최적화: Member 테이블에 대해 1회 BULK INSERT 실행 후 연산으로 PK를 설정하고, 해당 PK를 외래키로 사용하여 Article 테이블에 2번째 BULK INSERT 실행
- flatMap()과 stream() 조합으로 중첩 구조 데이터 전개: 외부에서 인입되는 1:N 관계의 JSON 데이터를 평탄화하여 BULK INSERT 대상 리스트 생성
Impact
- JPA 방식 대비 총 실행 시간 97.8% 단축: 78초 → 1.7초 (Member 10,000건 + Article 약 30,000건 기준)
- DB 부하 대폭 감소: 개별 INSERT 40,000회 이상에서 BULK INSERT 2회로 감소
Key Takeaway
BULK Write 성능 최적화 시 ORM의 편의성과 Raw SQL의 성능 사이의 명확한 트레이드오프를 인식하고, Auto Increment의 순차적 증가 특성을 활용한 PK 연산은 외래키 구조에서도 BULK INSERT의 성능 이점을 유지할 수 있는 실무 기법이다.
실천 포인트
Spring Data JPA를 사용하는 백엔드 서비스에서 10,000건 이상의 대량 데이터를 부모-자식 관계로 삽입해야 할 때, JPA의 saveAll() 대신 JdbcTemplate의 batchUpdate()를 도입하고 last_insert_id() 기반 PK 연산으로 외래키를 설정하면 쿼리 실행 횟수를 수십 배에서 수 배 수준으로 감소시킬 수 있다.