개요
Spring data repository의 saveAll() 메서드는 그 이름 덕분인지, 여러 개의 데이터들을 하나의 쿼리로 처리할 것만 같다. 하지만 실제 적용해보니 예상과는 다르게 동작하는 부분들이 있었기에 기록으로 남겨보려 한다.
문제
customerCreditTransactionRepository.saveAll(creditTransactions);
100개 정도 되는 데이터를 한 번에 저장할 일이 있었다. 나는 위 코드처럼 saveAll()을 통해서 하나의 쿼리로 처리할 수 있을 줄 알았다. 하지만 실제 동작은 데이터를 각각의 INSERT 쿼리로 처리하는 것 아닌가...
원인
왜 saveAll() 메서드가 한 번에 쿼리로 처리하지 못하는 것일까? 원인은 JPA의 기본키 생성 전략이였다.
@Entity
@Table(name = "customer_credit_transactions")
@Getter
@NoArgsConstructor
public class CustomerCreditTransaction extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
.
.
.
}
엔티티의 ID 생성 전략은 IDENTITY 였다. 해당 전략은 DB에 AUTO_INCREMENT 방식으로 생성된 PK를 채번하는 방식이다. IDENTITY 방식은 DB에게 ID 생성을 위임할 수 있어서 많이 사용하는 방식이지만, saveAll() 메서드의 bulk insert 방식을 정상 동작하지 못하게 한다.
JPA에서는 엔티티를 저장하게 되면 ID를 포함한 엔티티를 영속성 컨텍스트에 저장해야 한다. 그런데 ID를 갖고오기 위해서는 DB에 쿼리를 날려서 데이터를 저장하고 ID를 갖고와야 하는 상황인 것이다.
이러한 동작 방식 덕분에 여러 개의 INSERT 쿼리를 Batch로 묶을 수 없는 것이다.
또 다른 ID 생성 전략인 SEQUENCE, TABLE 방식은 INSERT 구문 전에 미리 기본키를 가져올 수 있다
해결
생성 전략을 바꿀 수도 있었겠지만, 이미 개발이 진행된 상태에서 해당 로직을 위해 생성 전략을 바꾸고 싶지는 않았다.
저장된 데이터를 영속성 컨텍스트에서 관리할 필요는 없었기에, JPA의 saveAll() 대신 JDBC를 통해 직접 bulk insert 하는 방법이 낫다고 판단하였다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
당연하지만, JDBC 의존성을 추가해주고
jdbc:mysql://localhost:3306/gudoknote?rewriteBatchedStatements=true
datasource의 db url 후미의 rewriteBatchedStatements=true을 붙여준다.
해당 문자열을 추가해주지 않으면, JDBC에서 쿼리를 배치로 처리하지 않는다.
@RequiredArgsConstructor
public class CustomerCreditTransactionJdbcRepositoryImpl implements
CustomerCreditTransactionJdbcRepository {
private final JdbcTemplate jdbcTemplate;
private final String CREDIT_TRANSACTION_BULK_INSERT_URL =
"INSERT INTO customer_credit_transactions "
+ "(amount, balance, reason, type, expires_at, customer_id, created_at, modified_at) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
@Override
public void bulkInsertCreditTransaction(
List<CreditTransactionSaveQueryDto> creditTransactionDtos) {
jdbcTemplate.batchUpdate(CREDIT_TRANSACTION_BULK_INSERT_URL,
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
CreditTransactionSaveQueryDto queryDto = creditTransactionDtos.get(i);
ps.setLong(1, queryDto.amount());
ps.setLong(2, queryDto.amount());
ps.setString(3, queryDto.reason());
ps.setString(4, CustomerCreditType.POINT.name());
ps.setTimestamp(5, Timestamp.valueOf(queryDto.expiresAt()));
ps.setLong(6, queryDto.customerId());
ps.setTimestamp(7, Timestamp.valueOf(queryDto.createdAt()));
ps.setTimestamp(8, Timestamp.valueOf(queryDto.createdAt()));
}
@Override
public int getBatchSize() {
return creditTransactionDtos.size();
}
});
}
}
saveAll() 메서드 대신, 데이터를 저장하는 쿼리와 코드를 정성스럽게 작성해준다.

Executing SQL batch update 이후 INSERT 쿼리문이 보이는 것으로 보아, 아주 잘 동작한 것으로 보인다.
'JPA' 카테고리의 다른 글
| [JPA] @SQLRestriction으로 soft delete 구현하기 (0) | 2025.12.21 |
|---|---|
| [JPA] 프록시 객체와 OneToOne 양방향 관계 이슈 (0) | 2025.12.21 |
| [JPA] 영속성 컨텍스트와 flush 시점 (0) | 2025.12.20 |
| [JPA] 테스트를 통해 알아본 JPA와 영속성 컨텍스트 (0) | 2024.03.15 |
| [Query DSL] Expressions를 사용하여 여러 값에 대한 존재 여부 확인하기 (0) | 2023.12.14 |