피드로 돌아가기
BULK 처리 Write에 집중해서 개선해보기
컬리 기술블로그컬리 기술블로그
Backend

BULK 처리 Write에 집중해서 개선해보기

Kurly 백엔드팀이 BULK Write에서 JPA의 saveAll()을 JDBC batchUpdate로 교체하고 Auto Increment PK 연산을 활용해 10,000건 Member + 30,000건 Article 삽입 시간을 78초에서 1.7초로 단축

2023년 9월 21일8intermediate

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 연산으로 외래키를 설정하면 쿼리 실행 횟수를 수십 배에서 수 배 수준으로 감소시킬 수 있다.

원문 읽기