[JPA] Batch Insert

2024. 11. 5. 23:54·Spring Framework/JPA
반응형
  • 대량의 데이터를 데이터베이스에 삽입하는 작업은 자주 접할 수 있는 이슈
  • Spring Boot, PostgreSQL을 사용할 때 대량 데이터 삽입 성능을 최적화하는 방법과 실험을 통해 비교 분석
  • JDBC Template을 직접 사용한 배치 삽입이 JPA Repository 방식보다 최대 76.3% 더 빠른 성능을 보였다.
  • PostgreSQL의 `reWriteBatchedInserts` 옵션 활성화만으로도 상당한 성능 향상을 얻을 수 있었다.

실습

  • Spring Boot, Spring Data JPA, PostgreSQL 환경에서 대량 데이터를 삽입할 때 사용할 수 있는 여러 방법의 성능을 비교
    • 개별 엔티티 저장(Repository.save())
    • 엔티티 컬렉션 저장(Repository.saveAll())
    • JDBC 템플릿을 활용한 배치 삽입 (batchInsert())
  • PostgreSQL JDBC 드라이버의 reWriteBatchedInserts 옵션과 배치 크기의 영향

엔티티와 데이터를 저장하는 메서드는 다음과 같다. ID 생성 전략을 IDENTITY로 설정하면 batch 옵션이 동작하지 않으므로 주의해야 한다.

@Entity
@SequenceGenerator(
        name = "issued_coupon_code_gen",
        sequenceName = "issued_coupon_code_coupon_code_sn_seq",
        initialValue = 1,
        allocationSize = 100
)
public class IssuedCouponCode {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "issued_coupon_code_gen")
    private Long couponCodeSn;

    private String serialCode;
}

@Transactional
public void save(int size) {
    List<IssuedCouponCode> issuedCouponCodes = generateCode(size);
    issuedCouponCodes.forEach(issuedCouponCodeRepository::save);
}

@Transactional
public void saveAll(int size) {
    List<IssuedCouponCode> issuedCouponCodes = generateCode(size);
    issuedCouponCodeRepository.saveAll(issuedCouponCodes);
}

@Transactional
public void batchInsert(int size) {
    List<IssuedCouponCode> issuedCouponCodes = generateCode(size);
    issuedCouponCodeRepository.batchInsert(issuedCouponCodes);
}

 

데이터 추가 방식들

Repository.save() - 개별 저장 방식

가장 기본적인 방식으로 각 엔티티를 개별적으로 저장

 

  • 각 save() 호출마다 개별 INSERT 문 실행
  • JPA의 완전한 엔티티 생명주기 관리 적용
  • 영속성 컨텍스트에 모든 엔티티 보관 (메모리 사용량 증가)

Repository.saveAll() - 컬렉션 저장 방식

여러 엔티티를 한 번에 저장하는 방식

  • 내부적으로는 여전히 개별 save() 호출
  • 하지만 Hibernate의 배치 처리 최적화 적용 가능
  • hibernate.jdbc.batch_size 설정에 따른 배치 그룹화

JDBC Template batchInsert() - 직접 배치 처리

JDBC Template을 사용해 직접 배치 삽입을 구현하는 방식

  • JPA/Hibernate 레이어 우회
  • PreparedStatement의 addBatch() 직접 활용
  • 최소한의 메모리 오버헤드로 순수 SQL 실행

옵션

# application.properties

# hibernate 배치 설정
hibernate.jdbc.batch_size=100
hibernate.order_inserts=true
hibernate.order_updates=true
hibernate.jdbc.batch_versioned_data=true

# reWriteBatchedInserts 옵션
jdbc:postgresql://localhost:5432/testdb?reWriteBatchedInserts=false
jdbc:postgresql://localhost:5432/testdb?reWriteBatchedInserts=true

 

hibernate.jdbc.batch_size를 100으로 설정 후 10만건의 데이터를 추가하는데 걸리는 시간을 측정했다.

 

  • 테스트 데이터: 10만 건의 쿠폰 코드 엔티티
  • 측정 지표: 총 실행 시간 (밀리초)
  • 반복 횟수: 각 방식별 3회 측정 후 평균값 사용
  • 환경 통제: 동일한 하드웨어, 네트워크, DB 상태에서 측정

결과

 

삽입 방식 reWriteBatchedInserts=false reWriteBatchedInserts=true 성능 향상
save 6524ms 5513ms 15.5%
saveAll 5062ms 3498ms 30.9%
batchInsert 2537ms 601ms 76.3%

 

save() vs saveAll() 비교

  • saveAll()이 약 23% 더 빠른 성능 (reWriteBatchedInserts=true)
  • 메서드 호출 오버헤드
  • save()와 saveAll() 모두 배치 처리가 적용되었다.
    • 동일한 @Transactional 범위 내에서 save()와 saveAll() 모두 배치 처리가 적용
    • Spring의 트랜잭션 관리 : 트랜잭션 종료 시점에 영속성 컨텍스트의 모든 변경사항이 flush되면서 배치 처리 효과를 얻게 된다.

JPA vs JDBC 비교

  • JDBC batchInsert()가 JPA 방식보다 압도적으로 빨랐다.
  • JPA의 오버헤드 요소들:
    • 엔티티 생명주기 관리
    • 1차 캐시에 10만 개 객체 저장
    • 더티 체킹 수행
    • 영속성 컨텍스트 플러시

PostgreSQL JDBC 드라이버의 reWriteBatchedInserts 가 배치 삽입 쿼리의 성능에 상당한 영향을 주는 것을 확인할 수 있었다. true로 설정할 경우 여러 개별 삽입 문을 하나의 다중 행 삽입 문으로 만들어 준다.

-- reWriteBatchedInserts=false
INSERT INTO issued_coupon_code(serial_code) VALUES('1');
INSERT INTO issued_coupon_code(serial_code) VALUES('2');
INSERT INTO issued_coupon_code(serial_code) VALUES('3');

-- reWriteBatchedInserts=true
INSERT INTO issued_coupon_code(serial_code) VALUES('1'),('2'),('3');

 

이렇게 다중 행을 한번에 처리함으로써 성능 향상을 극적으로 보였다.

  • SQL 파싱 오버헤드 최소화 (1회 vs 10만회)
  • 네트워크 전송량 감소
  • 데이터베이스 내부 최적화 가능

성능이 중요한 대량 처리 로직에서는 JPA Repository를 사용하는 것 대신 JDBC 기반의 batchInsert() 방식을 선택하는 것이 최적의 선택일 수 있다.

물론 대량 데이터 처리는 단순히 빠르기만 하면 되는 것이 아니라 비즈니스 요구사항, 시스템 안정성, 유지보수 등 다양한 상황을 고려해서 선택해야한다. 

반응형
저작자표시 비영리 변경금지 (새창열림)
'Spring Framework/JPA' 카테고리의 다른 글
  • [JPA] 5개 데이터가 4+1개 쿼리로 나누어지다.
  • [JPA] orphanRemoval, CasecaseType.REMOVE
덴마크초코우유
덴마크초코우유
IT, 알고리즘, 프로그래밍 언어, 자료구조 등 정리
    반응형
  • 덴마크초코우유
    이것저것끄적
    덴마크초코우유
  • 전체
    오늘
    어제
    • 분류 전체보기 (126)
      • Spring Framework (10)
        • Spring (5)
        • JPA (3)
        • Spring Security (0)
      • Language (52)
        • Java (12)
        • Python (10)
        • JavaScript (5)
        • NUXT (2)
        • C C++ (15)
        • PHP (8)
      • DB (16)
        • MySQL (10)
        • Reids (3)
        • Memcached (2)
      • 개발 (3)
      • 프로젝트 (3)
      • Book (2)
      • PS (15)
        • 기타 (2)
        • 백준 (2)
        • 프로그래머스 (10)
      • 딥러닝 (8)
        • CUDA (0)
        • Pytorch (0)
        • 모델 (0)
        • 컴퓨터 비전 (4)
        • OpenCV (1)
      • 기타 (16)
        • 디자인패턴 (2)
        • UnrealEngine (8)
        • ubuntu (1)
        • node.js (1)
        • 블로그 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    언리얼엔진4
    블루프린트
    웹
    Unreal Engine
    FPS
    Python
    MySQL
    클래스
    JS
    자바
    php
    CPP
    redis
    프로그래머스
    딥러닝
    select
    pytorch
    알고리즘
    JavaScript
    map
    게임
    Unreal
    게임 개발
    NUXT
    memcached
    PS
    mscoco
    C
    파이썬
    C++
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
덴마크초코우유
[JPA] Batch Insert
상단으로

티스토리툴바