0. 들어가기 전
JPA를 사용해서 개발 시 여러 건의 Entity를 INSERT를 해야하는 상황이 있었습니다.
이때, Spring Data JPA의 커스텀 메소드인 saveAll을 사용하면 Batch Insert 상태로 동작할 것이라고 예상했습니다.
하지만 실제 쿼리는 Batch Insert로 1건의 쿼리가 아닌 여러 건의 INSERT 쿼리가 발생하는 것을 확인했습니다.
그래서 이번에 JPA, PostgreSQL에서 Batch Insert를 어떻게 할 수 있는지 알아보도록 하겠습니다.
1. 문제 상황
@Transactional
public List<Skill> createAll(Collection<Skill> skills, String jobId) {
...
skillRepository.saveAll(entities);
...
}
위처럼 Spring Data JPA의 saveAll을 했을때, 예상과 달리 1건씩 쿼리가 나가는 것을 확인할 수 있었습니다.
* Hibernate 로그
insert
into
"job"."skill" (...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
insert
into
"job"."skill" (...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
insert
into
"job"."skill" (...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
* DB 로그
2025-01-22 04:18:15.836 UTC [511] LOG: insert into "job"."skill" (...) values (...)
2025-01-22 04:18:15.836 UTC [511] LOG: insert into "job"."skill" (...) values (...)
2025-01-22 04:18:15.837 UTC [511] LOG: insert into "job"."skill" (...) values (...)
2. Batch Insert 설정 (PostgreSQL)
원했던 쿼리는 Multi-Value 쿼리로 여러 INSERT가 Batch Insert로 1개의 쿼리로 묶어서 나가는 것을 원했습니다.
* 기본 insert - 쿼리 N개
INSERT INTO skill (...) VALUES (...)
INSERT INTO skill (...) VALUES (...)
INSERT INTO skill (...) VALUES (...)
* Batch Insert - 쿼리 1개
INSERT INTO skill (...)
VALUES (...), (...), (...)
해당 Batch Insert를 하기 위해서는 결론적으로 다음과 같은 2가지 설정을 하면 됩니다.
- Hibernate의 'hibernate.jdbc.batch_size' 설정
- JDBC PostgreSQL Driver의 'reWriteBatchedInserts' 설정
* application.yml
spring:
jpa:
properties:
hibernate.jdbc.batch_size: 50
datasource:
url: jdbc:postgresql://localhost:5432/batch_test?rewriteBatchedInserts=true
이렇게 설정하게 되면 다음과 같은 로그가 찍히게 됩니다.
* Hibernate 로그
insert
into
"job"."skill" (...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
insert
into
"job"."skill" (...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
insert
into
"job"."skill" (...)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
* DB 로그
2025-01-22 07:43:20.092 UTC [1212] LOG: insert into "job"."skill" (...) values (...),(...)
2025-01-22 07:43:20.093 UTC [1212] LOG: insert into "job"."skill" (...) values (...)
로그를 보면 Hibernate 로그는 그대로고 DB 로그는 3건 중에 2건이 Multi-Value 쿼리가 되고 1건은 따로 나간 것을 확인할 수 있습니다.
(3건의 INSERT 시)
결과만 보면 조금 이상해보일 수 있지만 결론적으로는 제대로 Multi-Value 쿼리가 실행되어 Batch Insert가 되었습니다.
아래에서 2가지 설정의 원리를 살펴봅시다.
3. Batch Insert 설정 원리
결론적으로 Batch Insert로 처리하긴 했지만, 처음 문제를 마주쳤을 때 저는 자세한 원리를 이해하지 못했었습니다.
해당 원리를 이해하기 위해서는 다음과 같은 3가지 개념의 역할을 이해해야 합니다.
- JPA의 쓰기 지연 저장소
- Hibernate의 batch_size 설정
- JDBC PostgreSQL Driver의 reWriteBatchedInserts 설정
3-1. JPA 쓰기지연 저장소
맨 처음에 JPA를 사용한 Batch Insert를 생각했을 때, 쓰기지연 저장소가 flush 시에 모은 여러 쿼리를 날리는 역할을 하니까
아무런 설정 없이 JPA의 기본 동작으로만으로도 여러 쿼리가 1개의 쿼리로 Batch Insert가 동작할 줄 알았습니다.
하지만, JPA의 쓰기지연 저장소의 역할은 'flush 시점에 쿼리를 DB에 반영하기 위해 쿼리들을 저장소에 모아주는 역할'입니다.
따라서, 모은 쿼리가 1번에 DB에 전송되는 것이 아니라 결국 1개씩 DB에 전송하게 됩니다.
그래서 모은 쿼리가 10개라면 쿼리를 DB에 전송하고 응답받는 네트워크 왕복 비용이 10번 발생하게 되는 것입니다.
3-2. Hibernate의 batch_size 설정
해당 Batch Size 설정은 '여러 쿼리를 모아서 전송해주는 역할'입니다.
쿼리가 10개일 때 DB에 한번에 10개의 쿼리를 전송하기 때문에 네트워크 왕복 비용을 줄일 수 있습니다.
하지만 이는 전송하고 응답받는 네트워크 왕복 비용만 줄일 뿐, 실제로 발생하는 쿼리가 1개의 쿼리로 Multi-Value 쿼리가 되지 않습니다.
3-3. JDBC PostgreSQL Driver의 reWriteBatchedInserts 설정
해당 reWriteBatchedInserts의 역할은 '전송된 쿼리를 내부적으로 파싱 및 실행 최적화를 통해 Multi-Value 쿼리로 재작성 하는 역할'입니다.
그래서 여러 건의 쿼리가 모아져서 전송 된다면, 해당 쿼리들을 내부적으로 파싱하여 Multi-Value 쿼리, Batch Insert 쿼리를 완성합니다.
그러므로, Batch Size를 설정하지 않아서 1건의 쿼리씩만 온다면 해당 옵션을 설정하더라도 애초에 Batch Insert가 되지 않는 것입니다.
Reference : https://jdbc.postgresql.org/documentation/use/
4. Batch Insert 주의할 점, 고려할 점
위에서 간단하게 Batch Insert 설정 방법과 원리들을 살펴봤지만, 추가적인 고려할 점이 몇 가지 존재합니다.
4-1. 쿼리의 수가 Batch Size보다 작으면 무조건 1개의 쿼리로 묶어서 전송될까?
위의 실제 동작 쿼리를 유심히 보면 알 수 있지만, 3건의 쿼리를 보낼 때 Batch Size를 3보다 크게 설정했음에도
Multi-Value 쿼리가 2건, 1건으로 나가서 총 2개의 쿼리가 발생한 것을 알 수 있습니다.
* DB 로그
2025-01-22 07:43:20.092 UTC [1212] LOG: insert into "job"."skill" (...) values (...),(...)
2025-01-22 07:43:20.093 UTC [1212] LOG: insert into "job"."skill" (...) values (...)
해당 부분은 레퍼런스를 아무리 찾아봐도 제대로 된 문서를 찾을 수가 없어서 경험적으로 결과를 보고 추론하게 되었습니다.
결과적으로는 쿼리를 묶는 Batch 단위는 쿼리 개수에서 2의 제곱수 단위로 묶어진다는 것을 확인했습니다.
- EX) 쿼리 11개 -> value 8건 + value 2건 + value 1건 (쿼리 총 3개)
(관련 레퍼런스를 찾으면 추가하겠습니다 ㅠ_ㅠ,, 아시는 분은 댓글로 알려주세요!)
4-2. JPA ID 생성 전략에 따라 Batch Insert가 불가능하다.
JPA에서는 ID 생성 전략을 기본적으로 @GeneratedValue 어노테이션의 속성을 통해 지정합니다.
크게 Enum으로 생성 전략을 지정하는데, 다음과 같습니다.
public enum GenerationType {
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using an underlying
* database table to ensure uniqueness.
*/
TABLE,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using a database sequence.
*/
SEQUENCE,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using a database identity column.
*/
IDENTITY,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity by generating an RFC 4122
* Universally Unique IDentifier.
*/
UUID,
/**
* Indicates that the persistence provider should pick an
* appropriate strategy for the particular database. The
* <code>AUTO</code> generation strategy may expect a database
* resource to exist, or it may attempt to create one. A vendor
* may provide documentation on how to create such resources
* in the event that it does not support schema generation
* or cannot create the schema resource at runtime.
*/
AUTO
}
이 중에서 IDENTITY 전략을 사용하게 되면 Batch Insert가 이루어지지 않습니다.
직접적인 원인으로는 IDENTITY 전략을 사용하게 되면 쿼리가 쓰기지연 저장소를 거치지 않고 바로 DB로 전송되기 때문입니다.
IDENTITY 전략은 PK 생성을 DB에 위임하기 때문에 DB를 거쳐야만 PK가 지정됩니다.
따라서 영속성 컨텍스트의 1차 캐시에 해당 Entity를 저장하기 위해서는 DB를 거쳐 PK를 받아와야 저장이 가능하기 때문에
쓰기지연 저장소를 거치지 않고 바로 DB로 전송하여 쿼리를 전송합니다.
이는 Batch Size를 지정하지 않은 것처럼 동작하여 reWriteBatchedInserts 옵션이 있더라도
DB에서 내부적으로 1건의 쿼리씩 전송받기 때문에 파싱하여 Multi-Value 쿼리를 만들 수 없습니다.
따라서, JPA에서 Batch Insert를 사용하기 위해서는 IDENTITY 방식이 아닌 Sequence나 Table, UUID 방식을 사용해야 합니다.
또는 엔티티 생성 시에 PK가 할당되는 별도의 방식을 사용해야 합니다.
저는 Tsid를 사용하여 엔티티 생성 시 PK가 할당되어 문제가 없었습니다.
Tsid에 관한 내용은 이전에 제가 작성한 ID 생성 전략 비교 글에서 조금 더 알아볼 수 있습니다!
https://ksh-coding.tistory.com/157#5.%20TSID-1
'Spring > JPA' 카테고리의 다른 글
JPA에서 delete, insert 시 왜 insert 쿼리가 먼저 실행될까? (1) | 2025.01.28 |
---|---|
[JPA] JPA, Spring Data JPA의 내부 동작 원리 알아보기 (0) | 2024.08.05 |
테스트 시 @ElementCollection 테이블 동적으로 가져오기(테스트 데이터 초기화) (0) | 2024.05.09 |
[JPA] JPA N+1 문제 및 근본적인 원인에 대한 개인적인 고찰 (6) | 2024.04.05 |
[JPA] JPA 1:N 관계에서 연관관계 주인을 1 대신 N에 두는 이유 (4) | 2023.08.06 |