백엔드 면접 용어 사전
Spring·JPAdirty checking

변경 감지

스냅샷과 비교해 바뀐 필드를 찾아 UPDATE를 자동 생성하는 기능. 그래서 setter만으로 반영된다.

영속 상태 엔티티의 필드가 바뀌면 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면 스냅샷을 만들지 않아 변경 감지도 동작하지 않는다. 대량 조회에서 메모리를 크게 아끼지만, 실수로 붙이면 수정이 조용히 무시된다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Spring·JPA — IoC/DI·AOP·영속성 컨텍스트·N+1