[스프링] 스프링 Data JPA(5) 벌크성 수정쿼리

2025. 8. 21. 18:11·스프링 데이터 JPA

이번 글에서는 스프링 데이터 JPA에서 벌크성 수정 쿼리를 어떻게 다루는지 살펴본다.
일반적인 엔티티 수정은 JPA가 영속성 컨텍스트를 관리하면서 Dirty Checking(변경 감지)을 통해 자동으로 update 쿼리를 실행한다.


하지만 대량의 데이터를 한 번에 수정해야 할 때는 이런 방식이 비효율적이다. 엔티티를 전부 조회해서 1건씩 변경하는 것은 메모리와 쿼리 성능 모두에 큰 부담이 된다.

이럴 때 필요한 것이 바로 벌크성 수정 쿼리다.

 

1. 순수 JPA에서 벌크성 수정 쿼리

순수 JPA에서는 EntityManager.createQuery()를 통해 직접 벌크 update 쿼리를 실행할 수 있다.

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;
}

executeUpdate()는 수정된 row 수를 반환한다.

 

테스트 코드:

@Test
public void bulkUpdate() throws Exception {
    // 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);
}

 

2. 스프링 데이터 JPA에서 벌크성 수정 쿼리

스프링 데이터 JPA는 @Modifying 어노테이션을 제공한다. 이 어노테이션은 쿼리가 select가 아닌 수정/삭제 DML 쿼리임을 명시한다.

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

테스트 코드:

@Test
public void bulkUpdate() throws Exception {
    // 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);

    // then
    assertThat(resultCount).isEqualTo(3);
}

 

3. 주의할 점

벌크 연산은 JPA의 영속성 컨텍스트를 무시하고 바로 DB에 쿼리를 실행한다.
따라서 벌크 연산 이후에도 영속성 컨텍스트에는 과거 값이 남아 있을 수 있다.

예를 들어, age >= 20인 회원 나이가 전부 +1 되었지만, 이후 findById()로 조회하면 영속성 컨텍스트에 남아 있던 기존 값이 반환될 수 있다.

이 문제를 해결하는 방법은 두 가지다.

  1. 벌크 연산 전에 영속성 컨텍스트를 비운다.
  2. 벌크 연산 직후 em.clear()로 초기화한다.

스프링 데이터 JPA에서는 @Modifying(clearAutomatically = true) 옵션을 주면 벌크 연산 직후 자동으로 영속성 컨텍스트를 clear해 준다.

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

마무리하며

벌크 연산은 영속성 컨텍스트를 무시하고 곧바로 DB에 반영하기 때문에 대량 업데이트나 삭제 작업에서 압도적으로 유리하다. 반대로 변경 감지를 이용한 개별 업데이트는 트랜잭션 안에서 엔티티 단위로 안정적으로 동작하지만, 데이터가 많아질수록 성능 부담이 커진다.

 

따라서 “실시간 비즈니스 로직”에서 엔티티 상태를 일관되게 유지하려면 변경 감지를 쓰고, “대량 데이터 일괄 처리”가 필요하다면 벌크 연산을 활용하는 것이 바람직하다. 상황에 따라 두 방식을 적절히 조합하는 전략이 중요할 것이다.

 

감사합니다.

'스프링 데이터 JPA' 카테고리의 다른 글

[스프링] 스프링 Data Jpa(7) 사용자 정의 리포지토리 구현  (1) 2025.08.26
[스프링] 스프링 Data JPA(6) @EntityGraph  (0) 2025.08.21
[스프링] 스프링 Data JPA(4) JPA 페이징과 정렬  (0) 2025.08.21
[스프링] 스프링 Data JPA(3) @Query, 리포지토리 메소드에 쿼리 정의하기  (0) 2025.08.21
[스프링] 스프링 Data JPA(2) 메소드 이름으로 쿼리생성  (1) 2025.08.21
'스프링 데이터 JPA' 카테고리의 다른 글
  • [스프링] 스프링 Data Jpa(7) 사용자 정의 리포지토리 구현
  • [스프링] 스프링 Data JPA(6) @EntityGraph
  • [스프링] 스프링 Data JPA(4) JPA 페이징과 정렬
  • [스프링] 스프링 Data JPA(3) @Query, 리포지토리 메소드에 쿼리 정의하기
0kingki_
0kingki_
자바 + 스프링 웹 개발
  • 0kingki_
    0kingki_
    0kingki_
  • 전체
    오늘
    어제
    • 분류 전체보기 (134)
      • 코딩 테스트 (54)
      • 자바 (21)
      • 스프링 (27)
      • 타임리프 (16)
      • 스프링 데이터 JPA (8)
      • 최적화 (2)
      • QueryDSL (4)
      • AWS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    BFS
    SpringDataJpa
    SOLID
    spring
    thymeleaf
    fetch join
    쿼리
    예외 처리
    예외처리
    스프링 데이터 JPA
    코딩 테스트
    JPA
    컬렉션
    객체지향
    QueryDSL
    다형성
    스프링
    스프링 컨테이너
    백준
    LocalDateTime
    코딩테스트
    Java
    최적화
    dfs
    mvc
    쿼리dsl
    재귀
    자바
    타임리프
    불변객체
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
0kingki_
[스프링] 스프링 Data JPA(5) 벌크성 수정쿼리
상단으로

티스토리툴바