JPA 에 대한 학습을 하던 중, @Modifying 어노테이션을 마주쳤다.
기존에 @Modifying 어노테이션을 사용하여 DB 삭제 작업을 처리해본 경험이 있다. 생각해보면, 이 때 동작 방식이나 원리를 확실하게 이해하지 못하고
'이걸 사용하면 DB 수정/삭제 쿼리를 날릴 수 있다'
정도만 알고 사용했다.
@Modifying 에 대해 알게 되면서, 궁금한 게 생겼다.
JPA 더티체킹 과
@Modifying이 함께 사용되면 어떻게 처리될까?
@Modifying 에 관한 간단한 정리와 더불어, 해당 부분에 대해서도 추가로 작성해보고자 한다.
update , delete 가 이에 해당JPA 는 더티 체킹에 의한 데이터 변경을 기본 전략으로 사용N+1 만큼의 쿼리가 실행 (SELECT 1번 + 변경 쿼리 N번)DB 상태 동기화 작업 필요@ModifyingJPA 에서 사용 가능한 벌크 연산 어노테이션@Query 와 함께 사용함으로 벌크 연산 구현 가능DB 쿼리 요청@Query 어노테이션@Query 어노테이션은 JPA 의 조회용 JPQL 생성 어노테이션JPA 의 데이터 변경 전략은 앞서 말했듯, 더티 체킹에 의한 변경@Query 에 update 나 delete 쿼리를 등록할 경우, 쿼리 자체가 비실행@Query 는 JPQL 을 항상 생성@Modifying 이 없다면, JPQL 을 조회 쿼리처럼 사용하려고 시도@Modifying 어노테이션은 @Query 와 함께 사용함으로, 해당 JPQL 쿼리는 DB 변경 쿼리임을 표시void : 변경 쿼리만 실행int : 변경 쿼리 적용 개수 반환clearAutomatically@Modifying(clearAutomatically = true) 같이 사용== em.clear()flushAutomatically@Modifying(flushAutomatically = true) 같이 사용== em.flush()Hibernate 는 em.flush() 가 호출되면, 쓰기 지연 버퍼에 저장된 쿼리를 SQL 로 변환 후 DB 로 요청Hibernate 에는 실행 전, 자동 플러시를 실행하는 몇 시점이 존재1. 명시적 플러시 호출
em.flush()2. 트랜잭션 커밋 시점
@Transactional 내의 모든 로직 실행 후, tx.commit() 직전3. JPQL 등 쿼리 실행 직전
DB 쿼리 실행 시 영속성 컨텍스트와 DB 상태 정합성 유지를 위해4. em.persist() 시점
GeneratedValue(strategy = GenerationType.IDENTITY) 전략 한정)ID 값 필드가 기본적으로 존재INSERT 후에야 ID 가 DB 에 의해 자동으로 생성되는 전략INSERT 실행 후 ID 를 가져와야, 엔티티 생성 시 초기화 가능💡
Hibernate의Flush모드
FlushModeType.AUTO :Hibernate 기본값flushFlushModeType.COMMIT :flushFlushModeType.MANUAL :flush 호출해야만 flushEntityManager 단위
EntityManager em;
em.setFlushMode(FlushModeType.COMMIT); // COMMIT 모드로 변경
트랜잭션 단위
@Transactional(flushMode = FlushModeType.COMMIT)
전역 설정
spring:
jpa:
properties:
jakarta.persistence.flushMode: COMMIT
📌 기본 모드가
AUTO이므로,flushAutomatically속성을 사용하지 않아도 쿼리 실행 전flush호출
@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);
}
Repoid 에 해당하는 Member 의 이름 변경 쿼리 실행member 등록member 의 이름을 "B" 로 변경JPA 더티체킹 대기repo.update() 실행member 의 이름을 "C" 로 변경em.flush() 로 영속성 컨텍스트 동기화(더티 체킹 업데이트 실행) 및 em.clear() 로 영속성 컨텍스트 초기화DB 에서 최신화된 Member 조회
INSERT 1번 + UPDATE 2번 + SELECT 1번em.clear() 로 인한 1번의 추가 SELECTname='B' 업데이트 실행 후 name='C' 업데이트 실행dbMember.getName() 결과 : "C"
NAME 컬럼이 "C" 로 잘 반영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번의 추가 SELECTdbMember.getName() 결과 : "C"
NAME 컬럼은 "C" , TEMP_CNT 컬럼은 100이 잘 반영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번의 추가 SELECTname='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() 를 하지 않아서 기존 영속성 컨텍스트에 엔티티가 남아있기 때문영속성 컨텍스트가 제 때 초기화되지 않으면 원하지 않는 결과를 초래할 수 있다!
@Modifying 과 영속성 컨텍스트 변경 작업을 같은 트랜잭션 내에서 진행하면 원하지 않은 결과를 초래할 수 있음clearAutomatically = true 속성을 반드시 추가해서 사용벌크 연산을 사용하려면 반드시 영속성 컨텍스트와 DB 상태를 잘 고려하여 사용하자!
검수)
- Google Gemini ( https://gemini.google.com/app )
- ChatGPT ( https://chatgpt.com/ )