pile·
백엔드·마켓컬리마켓컬리 Hello World·

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

대량 데이터 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 추론 패턴을 알려주는 실전 가이드.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2