@Modifying + 더티 체킹

랏 뜨·2025년 8월 16일

🔎 Overview

  JPA 에 대한 학습을 하던 중, @Modifying 어노테이션을 마주쳤다.

  기존에 @Modifying 어노테이션을 사용하여 DB 삭제 작업을 처리해본 경험이 있다. 생각해보면, 이 때 동작 방식이나 원리를 확실하게 이해하지 못하고

'이걸 사용하면 DB 수정/삭제 쿼리를 날릴 수 있다'

정도만 알고 사용했다.

  @Modifying 에 대해 알게 되면서, 궁금한 게 생겼다.

JPA 더티체킹@Modifying함께 사용되면 어떻게 처리될까?

  @Modifying 에 관한 간단한 정리와 더불어, 해당 부분에 대해서도 추가로 작성해보고자 한다.


1️⃣ 벌크 연산

  • 여러 데이터를 하나의 쿼리일괄 변경하는 것
  • update , delete 가 이에 해당

❓ 필요한 이유

  • JPA더티 체킹에 의한 데이터 변경을 기본 전략으로 사용
    • 이러한 전략은 데이터의 변경이 없을 경우, 불필요한 변경 쿼리를 발생시키지 않음
  • 하지만 변경할 엔티티가 여러 개인 경우, 해당 엔티티에 대한 변경 쿼리각각 1번씩 실행
    • 변경 데이터가 N개일 경우 : 총 N+1 만큼의 쿼리가 실행 (SELECT 1번 + 변경 쿼리 N번)
    • 네트워크 전송량의 증가로 인한 성능 저하 발생 가능
  • 벌크 연산을 사용하면, 단 1번의 추가 쿼리여러 데이터 변경 가능
    • 영속성 컨텍스트DB 상태 동기화 작업 필요


2️⃣ @Modifying

  • JPA 에서 사용 가능한 벌크 연산 어노테이션
  • @Query 와 함께 사용함으로 벌크 연산 구현 가능
  • 더티 체킹을 거치지 않고, 바로 DB 쿼리 요청

✍️ Learn With

1️⃣ @Query 어노테이션

  • @Query 어노테이션은 JPA조회용 JPQL 생성 어노테이션
    • JPA데이터 변경 전략은 앞서 말했듯, 더티 체킹에 의한 변경
    • 따라서 직접적으로 사용되는 쿼리는 일반적으로 조회용 쿼리
  • 단순히 @Queryupdatedelete 쿼리를 등록할 경우, 쿼리 자체가 비실행
    • @QueryJPQL 을 항상 생성
    • @Modifying 이 없다면, JPQL조회 쿼리처럼 사용하려고 시도
    • 오류 발생
  • @Modifying 어노테이션은 @Query 와 함께 사용함으로, 해당 JPQL 쿼리는 DB 변경 쿼리임을 표시
  • 반환 타입
    • void : 변경 쿼리만 실행
    • int : 변경 쿼리 적용 개수 반환

2️⃣ 속성

  • clearAutomatically
    • @Modifying(clearAutomatically = true) 같이 사용
    • 쿼리 실행 , 영속성 컨텍스트 초기화 자동 실행
    • == em.clear()
  • flushAutomatically
    • @Modifying(flushAutomatically = true) 같이 사용
    • 쿼리 실행 , 영속성 컨텍스트 동기화 자동 실행
    • == em.flush()

3️⃣ Hibernate 와 Flush

  • Hibernateem.flush() 가 호출되면, 쓰기 지연 버퍼에 저장된 쿼리를 SQL 로 변환 후 DB 로 요청
  • Hibernate 에는 실행 전, 자동 플러시를 실행하는 몇 시점이 존재

1. 명시적 플러시 호출

  • em.flush()

2. 트랜잭션 커밋 시점

  • @Transactional 내의 모든 로직 실행 후, tx.commit() 직전

3. JPQL 등 쿼리 실행 직전

  • DB 쿼리 실행 시 영속성 컨텍스트DB 상태 정합성 유지를 위해

4. em.persist() 시점

  • GeneratedValue(strategy = GenerationType.IDENTITY) 전략 한정)
    • 나머지 전략은 마찬가지로 쓰기 지연 버퍼에 작업을 쌓고, 바로 Flush는 실행하지 않음
  • 엔티티에는 ID 값 필드가 기본적으로 존재
  • 해당 전략은 INSERT 후에야 IDDB 에 의해 자동으로 생성되는 전략
  • 따라서 INSERT 실행 후 ID 를 가져와야, 엔티티 생성 시 초기화 가능

💡 HibernateFlush 모드

  1. FlushModeType.AUTO :
    • Hibernate 기본값
    • 쿼리 실행 flush
  2. FlushModeType.COMMIT :
    • 트랜잭션 커밋 시에만 flush
  3. FlushModeType.MANUAL :
    • 직접 flush 호출해야만 flush

  • 사용 방법
  1. EntityManager 단위

    EntityManager em;
    
     em.setFlushMode(FlushModeType.COMMIT);	// COMMIT 모드로 변경
  2. 트랜잭션 단위

    @Transactional(flushMode = FlushModeType.COMMIT)
  3. 전역 설정

    spring:
      jpa:
        properties:
          jakarta.persistence.flushMode: COMMIT

📌 기본 모드가 AUTO 이므로, flushAutomatically 속성을 사용하지 않아도 쿼리 실행 전 flush 호출



3️⃣ 더티 체킹 ➕ @Modifying

🔗 '사실상 이번 정리글 작성의 이유'

  • 그렇다면 더티 체킹@Modifying 을 함께 사용하면 어떻게 동작할까? 간단한 테스트 코드와 함께 확인해보자.

  • 최초 코드
@SpringBootTest
@Transactional
public class TempTest {

    @Autowired
    EntityManager em;

    @Autowired
    Repo repo;

    @Test
    @Rollback(value = false)	// DB 확인을 위한 롤백 미실시
    void t1() {
        Member member = new Member();
        member.setName("A");
        repo.save(member);

		// == member
        Member findMember = repo.findById(1L).get();
        findMember.setName("B");

        repo.update("C", findMember.getId());
        em.flush();
        em.clear();
        System.out.println("===================");

        Member dbMember = repo.findById(1L).get();
        System.out.println("findMember.getName() = " + findMember.getName());
        System.out.println("dbMember.getName() = " + dbMember.getName());
    }
}

interface Repo extends JpaRepository<Member, Long> {

    @Modifying
    @Query("update Member m set m.name = :name where m.id = :id")
    void update(@Param("name") String name, @Param("id") Long id);
}
  • 벌크연산을 수행하기 위한 Repo
    • id 에 해당하는 Member이름 변경 쿼리 실행
  • 이름이 "A"member 등록
  • member 의 이름을 "B" 로 변경
    • JPA 더티체킹 대기
  • repo.update() 실행
    • member 의 이름을 "C" 로 변경
  • em.flush()영속성 컨텍스트 동기화(더티 체킹 업데이트 실행)em.clear()영속성 컨텍스트 초기화
  • 1차 캐시가 아닌 DB 에서 최신화된 Member 조회
  • 영속성 컨텍스트 멤버의 이름과 최신화된 멤버의 이름 출력

1️⃣ 최초 코드 실행

  • 이름만 변경하는 최초 코드 실행
  • 실행 결과
  • INSERT 1번 + UPDATE 2번 + SELECT 1번
    • em.clear() 로 인한 1번의 추가 SELECT
  • name='B' 업데이트 실행 후 name='C' 업데이트 실행
    • 더티 체킹 업데이트 먼저 실행
    • 벌크 연산 추가 실행
  • dbMember.getName() 결과 : "C"

📌 결과

  • NAME 컬럼이 "C" 로 잘 반영

2️⃣ 벌크 연산 이전, 다른 필드 변경 코드 실행

  • setTempCnt이름이 아닌 필드 변경
  • repo.update() 호출 이전에 findMember.setTempCnt(100) 실행
// ...

Member member = new Member();
member.setName("A");
// setTempCnt 필드 추가
member.setTempCnt(0);
repo.save(member);

Member findMember = repo.findById(1L).get();
findMember.setName("B");
// update 호출 전, setTempCnt 필드 변경
findMember.setTempCnt(100);

repo.update("C", findMember.getId());
em.flush();
em.clear();

// ...
  • 실행 결과
  • INSERT 1번 + UPDATE 2번 + SELECT 1번
    • em.clear() 로 인한 1번의 추가 SELECT
  • 1번 실행 결과와 마찬가지로, 더티 체킹 업데이트 이후 벌크 연산 수행
  • dbMember.getName() 결과 : "C"

📌 결과

  • NAME 컬럼은 "C" , TEMP_CNT 컬럼은 100이 잘 반영

3️⃣ 벌크 연산 이후, 다른 필드 변경 코드 실행

  • repo.update() 호출 이후에 findMember.setTempCnt(100) 실행
// ...

Member findMember = repo.findById(1L).get();
findMember.setName("B");

repo.update("C", findMember.getId());
// update 호출 후, setTempCnt 필드 변경
findMember.setTempCnt(100);
em.flush();
em.clear();

// ...
  • 실행 결과
  • INSERT 1번 + UPDATE 3번 + SELECT 1번
    • em.clear() 로 인한 1번의 추가 SELECT
  • name='B' 업데이트 실행 후 name='C' 업데이트 실행
    • 최초 더티 체킹 업데이트 실행
    • 벌크 연산 추가 실행
  • name='B'temp_cnt=100 업데이트가 추가 실행
    • tempCnt 로 인한 더티 체킹 업데이트 추가 실행
  • dbMember.getName() 결과 : "B"

📌 결과

  • TEMP_CNT 컬럼은 100이 잘 반영
  • NAME 컬럼은 "C" 가 아닌, "B"가 최종 반영


‼️ dbMember.getName()C가 아닌 B로 변경됨

  • @Modifying 쿼리 실행 시점에 NAME 컬럼을 "C"로 업데이트
  • findMember.setTempCnt(100) 으로 기존 영속성 컨텍스트에 존재하는 엔티티tempCnt 속성 변경
    • 해당 엔티티는 더티 체킹에 의한 업데이트 대상으로 변경
    • 이 때 영속성 컨텍스트name 은 여전히 "B"
    • 아직 em.clear() 를 하지 않아서 기존 영속성 컨텍스트에 엔티티가 남아있기 때문
  • 원했던 변경된 이름 : "C"
  • 최종 변경된 이름 : "B"

영속성 컨텍스트가 제 때 초기화되지 않으면 원하지 않는 결과를 초래할 수 있다!



🔨 결론

  • @Modifying영속성 컨텍스트 변경 작업을 같은 트랜잭션 내에서 진행하면 원하지 않은 결과를 초래할 수 있음
  • 추가 비즈니스 로직 작성 시, clearAutomatically = true 속성을 반드시 추가해서 사용
  • 혹시 모를 Side Effect가 불안하다면, 애초에 트랜잭션을 분리해서 작업을 진행하도록 하자.

벌크 연산을 사용하려면 반드시 영속성 컨텍스트DB 상태를 잘 고려하여 사용하자!



검수)

profile
기록

0개의 댓글