영속 상태 엔티티의 필드가 바뀌면 JPA가 UPDATE를 스스로 만들어 주는 기능(dirty checking).
save()를 부르지 않아도 반영되는 이유다.
자바에는 "값이 바뀌었다"를 알려 주는 장치가 없다
m.setName("이") 는 그냥 필드 대입이다. JPA가 가로챌 수 없다.
그래서 처음 값을 따로 적어 두고 나중에 대조하는 우회로를 쓴다 — 그 적어 둔 값이 스냅샷이다.
메모리에서 벌어지는 일
① 조회 시점 — 힙에 두 덩어리가 생긴다.
- Member 인스턴스 @a1b2 : — id=1, name="김", age=20
- EntityEntry — : loadedState = [1, "김", 20] ← 스냅샷(별도 배열)
② m.setName("이") — 인스턴스만 바뀐다.
Member 인스턴스 @a1b2 : id=1, name="이", age=20 ← 바뀜
EntityEntry : loadedState = [1, "김", 20] ← 그대로
③ flush 시점 — 현재 값을 배열로 만들어 원소별로 비교한다.
현재 [1, "이", 20]
스냅샷 [1, "김", 20]
↑ index 1 이 다름 → UPDATE member SET name=?, age=? WHERE id=?
④ 전송 후 — 스냅샷을 방금 보낸 값으로 갱신한다. 그래서 같은 트랜잭션에서 다시 flush해도 중복 UPDATE가 나가지 않는다.
코드로 보면
@Transactional
void rename(Long id, String name) {
Member m = repository.findById(id).orElseThrow(); // ① 조회 + 스냅샷
m.setName(name); // ② 인스턴스만 변경
} // ③ 커밋 직전 flush → 비교 → UPDATE ④ 스냅샷 갱신
왜 save()가 필요 없나
이미 managed 상태이므로 컨텍스트가 알아서 추적한다. Spring Data JPA의 save()는
새 엔티티면 persist, 아니면 merge를 호출하는 편의 메서드일 뿐 — managed 엔티티에
부르는 것은 불필요한 호출이다.
함정
- detached는 감지되지 않는다. 트랜잭션 밖에서 바꾼 값은 스냅샷 자체가 없으므로 반영되지 않는다.
- 기본 UPDATE는 모든 컬럼을 보낸다. 바뀐 컬럼만 보내려면
@DynamicUpdate. 대신 SQL이 매번 달라져 하이버네이트가 미리 만들어 둔 SQL을 재사용하지 못한다. 컬럼이 아주 많거나 LOB이 있을 때만 검토한다. readOnly = true면 스냅샷을 만들지 않아 변경 감지도 동작하지 않는다. 대량 조회에서 메모리를 크게 아끼지만, 실수로 붙이면 수정이 조용히 무시된다.