벌크성 쿼리
벌크성 쿼리(Bulk Query)란 데이터베이스에서 한 번의 실행으로 대량의 데이터를 삽입, 수정, 삭제하거나 조회하는 쿼리를 의미한다.
주로 많은 데이터를 효율적으로 처리하기 위해 사용되며, 데이터 처리 속도를 높이고 시스템 리소스를 절약하는 데 유용하다.
예시로 모든 직원의 연봉을 10% 인상하는 쿼리를 짜야 한다면, 직원들을 하나씩 끌어서 10% 씩 인상하는 것보다는 DB에서 업데이트 쿼리를 한번만에 끝내고 커밋하는 것이 효율적일 것이다. 먼저 순수 JPA를 사용하여 벌크성 수정 쿼리를 처리하는 방법을 알아보자.
JPA를 사용한 벌크성 수정 쿼리
public int bulkAgePlus(int age) {
int resultCount = em.createQuery("update Member m set m.age = m.age + 1" +
" where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
- 회원의 나이가 파라미터로 들어온 값보다 크거나 같다면 나이에 + 1를 하는 벌크성 쿼리이다.
- executeUpdate() - 반환 값이 int 이다.
JPA를 사용한 벌크성 수정 쿼리 테스트
@Test
public void bulkUpdate() {
// given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 19));
memberJpaRepository.save(new Member("member3", 20));
memberJpaRepository.save(new Member("member4", 21));
memberJpaRepository.save(new Member("member5", 40));
// when
int resultCount = memberJpaRepository.bulkAgePlus(20);
// then
assertThat(resultCount).isEqualTo(3);
}
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
- org.springframework.data.jpa.repository.Modifying;
-
벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용해야 한다.
- 이 어노테이션이 있어야 JPA에서 executeUpdate()를 실행한다.
- 이게 없으면 getResultList()나 getSingleResult()를 호출해버리기 때문이다.
사용하지 않으면 다음 예외 발생
java.lang.IllegalStateException: Query executed via 'getResultList()' or 'getSingleResult()' must be a 'select' query [update Member m set m.age = m.age + 1 where m.age >= :age]
at org.hibernate.query.sqm.internal.QuerySqmImpl.verifySelect(QuerySqmImpl.java:352)
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:372)
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:143)
at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:275)
스프링 데이터 JPA를 사용한 벌크성 수정 쿼리 테스트
@Test
public void bulkUpdate() {
// given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
// when
int resultCount = memberRepository.bulkAgePlus(20);
// em.clear(); or @Modifying(clearAutomatically = true)
// 영속성 컨텍스트를 무시하고 실행
List<Member> result = memberRepository.findByUsername("member5");
Member member5 = result.get(0);
System.out.println("member5 = " + member5); // 40살
// then
assertThat(resultCount).isEqualTo(3);
}
주의점
- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true) (이 옵션의 기본값은 false)
- 이 옵션없이 회원을 findById 로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될수있다.
- 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자.
벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.
권장하는 방안
- 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
- 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.
'JPA' 카테고리의 다른 글
[Spring Data JPA] JPA Hint & Lock (0) | 2025.01.18 |
---|---|
[Spring Data JPA] @EntityGraph (0) | 2025.01.17 |
[Spring Data JPA] 페이징과 정렬 (0) | 2025.01.16 |
[JPA] OSIV와 성능 최적화 (0) | 2025.01.10 |
[JPA] 컬렉션 조회 최적화 (0) | 2025.01.10 |