[뚝배기] JPA 벌크연산 (feat.ID 생성 전략)
주문 생성 로직 중 Order에 포함된 다수의 OrderMenu를 저장하는 코드가 있다.
Order savedOrder = orderRepository.save(order);
for (OrderCreateRequestDto.MenuOrderDto item : request.menus()) {
Menu menu = menus.stream()
.filter(m -> m.getId().equals(item.menuId()))
.findFirst()
.orElseThrow();
OrderMenu orderMenu = OrderMenu.builder()
.order(savedOrder)
.menu(menu)
.count(item.count())
.build();
orderMenuRepository.save(orderMenu);
}
여기서 만약 메뉴가 100개... 혹은 지구 회식으로 주문에 속한 메뉴가 1억개라면? 그 수만큼 쿼리가 나가게 되므로 성능이 매우 안좋아질 것이다. saveAll()을 통해 orderMenu를 한 번에 저장할 수 있겠지만, 이 또한 벌크 연산으로 처리되지 않는다.
사실, 당장은 saveAll로 아주 많은 데이터를 저장할 일이 없을 것 같긴 하다. 100개의 메뉴를 주문하는 경우는 거의 없기 때문이다.
하지만, 왜 이렇게 작동하는지 확인하고 추후에 동일한 환경에서 bulk 연산을 처리해야 할 때 유연하게 대처할 수 있어야 하므로 벌크 연산에 대해 알아보도록 하자.
벌크 연산(수정, 삭제)
엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제하려면 EntityManaer.remove() 메소드를 사용한다. 하지만 이 방법으로는 수백개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸린다.
벌크 연산은 executeUpdate() 메소드를 사용한다. 단, 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의하여 사용하여야 한다. 따라서, 다음과 같은 상황에 주의하여야 한다!

SpringData Jpa에서는 @Modifying을 통해 벌크 연산이 수행된다!
같이 사용할 수 있는 속성은 다음과 같다.
- flushAutomatically: 기본 값은 false로, true면 해당 쿼리를 실행하기 전, 영속성 컨텍스트의 변경 사항을 DB에 flush한다.
- clearAutomatically: 기본 값은 false로, true면 해당 쿼리를 실행한 후, 영속성 컨텍스트를 clear한다. 이 옵션을 true로 설정하면 위에서 본 1차 캐시와의 동기화 문제가 해결된다!
❗ @Modifying 애노테이션은 기본적으로 @Transactional과 함께 사용된다. 변경 작업은 트랜잭션 내에서 실행되어야 하며, 완료되지 않은 변경 작업이 여러 작업에 영향을 줄 수 있기 때문이다.
벌크 연산(저장)
저장을 벌크 연산으로 처리하는 것에 대해 알아보기 위해서는, ID 생성 전략에 대해 알아야 한다.
| 전략 | ID 생성 주체 | 설명 | 특징 |
| IDENTITY | DB | DB가 AUTO_INCREMENT로 ID 부여 | JPA batch INSERT 불가 ❌ |
| SEQUENCE | DB + Hibernate | DB의 시퀀스 객체에서 Hibernate가 ID 미리 받아 사용 | JPA batch INSERT 가능 ⭕ |
| TABLE | JPA | 별도 테이블에서 ID 관리 (비추천) | 느림, 잘 안 씀 |
| UUID | 애플리케이션 | 앱이 UUID를 만들어서 ID로 씀 | 분산 시스템에 적합 |
Hibernate/JPA에서 batch insert 하고 싶다면 SEQUENCE 전략을 쓰는 게 정석이다. 하지만, 서비스 재시작 시 캐시가 날아감 → ID 건너뜀 현상으로 인한 리소스 낭비가 생기고, 또한 DB 간 이식성 떨어진다는 단점이 있다. ID 생성 전략을 바꾸기 쉽지 않은 점도 있다..
따라서 JdbcTemplate.batchUpdate()를 주로 사용한다!
jdbcTemplate.batchUpdate(
"INSERT INTO order_menu (order_id, menu_id, count) VALUES (?, ?, ?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
OrderMenuDto dto = orderMenus.get(i);
ps.setLong(1, dto.getOrderId());
ps.setLong(2, dto.getMenuId());
ps.setInt(3, dto.getCount());
}
@Override
public int getBatchSize() {
return orderMenus.size();
}
}
);
이를 통해 DB에 ID 생성 책임을 맡길 수 있다. 하지만, JPA를 사용하지 못한다는 단점이 있다.
벌크 연산의 단점
벌크 연산(batch)는 속도는 빠르지만, 정합성을 자동으로 지켜주지 않는다. 중간에 문제가 생기면 전체가 실패할 수도 있고, 다시 시도할 땐 모든 데이터를 다시 시도해야 하는 불편함이 있다.
따라서 데이터 정합성이 중요한 데이터라면, 속도는 느릴 수 있지만, 각 객체마다 정합성 검증이나 유효성 체크를 자연스럽게 할 수 있고, 실패해도 해당 건만 롤백하거나 재처리할 수 있는 단건 처리를 하는 경우도 많다.