
"왜 update 쿼리가 자동으로 나가지?", "save() 안 불러도 값이 바뀌네" 같은 착오를 겪기 쉽습니다.
이번 글에서는 그 이유인 더티 체킹(Dirty Checking)을 깊게 파고들어, 실무에서 자주 틀리는 사례와 성능/동작 원리를 정리합니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public void changeName(Long id) {
Member m = memberRepository.findById(id).orElseThrow();
m.setName("CHANGED");
// repository.save(m); <-- 주석으로 인해 호출하지 않음
}
}
로그:
Hibernate:
select member0_.id as id1_0_0_, member0_.name as name2_0_0_ from member member0_ where member0_.id=?
Hibernate:
update member set name=? where id=?
예상과 다르게
update가 자동으로 호출되는데 이유는 이전 글에서 확인했던 영속성 컨텍스트의 더티 체킹 기능 때문입니다.
@Transactional
public void modifyDetached(Long id) {
Member m = memberRepository.findById(id).orElseThrow();
em.detach(m); // detach
m.setName("WONT_SAVE");
}
UPDATE 없음 — detach 된 엔티티는 변경 추적 대상 ❌
flush() 시점에서 변경된 필드만 DB에 반영하는 JPA의 메커니즘입니다.동작 흐름(요약):
✅ 더티 체킹은 단순히 필드 변경을 감지하는 것이 아니라, 엔티티의 스냅샷(초기 상태) 과 현재 상태를 비교하는 과정입니다.
@Transactional
public void testFlushClear(Long id) {
Member m = memberRepository.findById(id).orElseThrow();
m.setName("A");
em.flush(); // 변경을 DB에 바로 반영
em.clear(); // 영속성 컨텍스트 초기화 -> m은 detached
m.setName("B"); // 트랜잭션 커밋 시점에 'B'는 반영되지 않음
}
flush()는 영속성 컨텍스트의 변경 내용을 DB와 동기화합니다.
clear()는 영속성 컨텍스트를 비워, 이후 변경은 더 이상 추적되지 않습니다.
flush()는 변경 내용을 DB와 동기화하지만 트랜잭션은 여전히 열려 있으며, 커밋이 아님을 주의해야 합니다.
set을 해도 DB에 반영되지 않습니다. 반영하려면 em.merge() 또는 repository.save()로 병합이 필요합니다.merge()는 관리 객체(관리되는 인스턴스)를 반환합니다. merge() 호출 후 반환값을 사용해야 안전합니다.Member detached = ...;
Member merged = em.merge(detached);
merged.setName("OK"); // 변경 추적 대상
⚠️
merge()후에도 기존 객체(detached)를 수정하면 반영되지 않습니다
@OneToMany(mappedBy = "parent") 처럼 inverse side만 수정하면 DB 반영 안 됨.public void addChild(Child c) {
children.add(c);
c.setParent(this); // owning side 변경
}
new ArrayList()로 교체하면(=참조 교체) 기존 추적이 끊길 수 있음.add()/remove()로 수정.UPDATE(bulk)는 영속성 컨텍스트를 거치지 않고 DB에 바로 반영.em.clear() 또는 영속성 컨텍스트를 수동으로 동기화.em.createQuery("update Member m set m.age = m.age + 1").executeUpdate();
em.clear();
flush()/clear() 주기적 호출, 혹은 DTO 조회나 bulk query 사용.@Transactional(readOnly = true) 사용 (읽기 최적화). ✅ 동작 세부는 구현체에 따라 다를 수 있음em.clear()로 영속성 컨텍스트 정리.batchSize)로 flush() + clear() 하여 메모리와 더티체킹 비용 관리.merge()를 사용하고, 반환값을 사용해 추가 작업.변경만 하면 자동 UPDATE
@Transactional
public void rename(Long id) {
Member m = em.find(Member.class, id);
m.setName("NEW");
}
// commit 시 UPDATE 발생
detach 하면 추적 종료
@Transactional
public void detachExample(Long id) {
Member m = em.find(Member.class, id);
em.detach(m);
m.setName("NOPE"); // 반영 안 됨
}
merge 사용법
@Transactional
public void mergeExample(Member detached) {
Member managed = em.merge(detached);
managed.setEmail("a@b.com"); // 변경 추적
}
bulk 업데이트 후 주의
@Transactional
public void incrAgeForAll() {
em.createQuery("update Member m set m.age = m.age + 1").executeUpdate();
em.clear(); //동기화
}
flush()와 commit 시점에 변경 감지 후 SQL 생성.detach()/clear()하면 변경 추적이 멈추고, merge()는 병합 후 반환값을 사용해야 안전.save()를 호출하지 않았는데도 UPDATE가 나가는 이유는?merge()와 save()의 차이는 무엇인가?