1. Dirty Checking이란
영속성 컨텍스트는 엔티티를 처음 조회하거나 persist할 때 해당 시점의 스냅샷을 내부에 보관합니다. 트랜잭션 커밋 시점에 flush가 호출되면서 현재 엔티티 상태와 스냅샷을 필드 단위로 비교해, 변경이 있으면 자동으로 UPDATE SQL을 생성합니다. 이것이 Dirty Checking입니다.
트랜잭션 커밋
↓
entity manager의 flush 자동 호출
↓
dirty checking (현재 상태 vs 스냅샷 비교)
↓
변경 감지 → UPDATE SQL 생성 → DB 전송
↓
JDBC commit (영구 반영)
따라서 같은 트랜잭션 내에서 엔티티를 조회하고 수정하면, save() 호출 없이도 트랜잭션 커밋 시 UPDATE가 자동으로 발생합니다.
@Transactional
public void updateUserName(Long userId, String newName) {
User user = userRepository.findById(userId).orElseThrow();
user.setName(newName);
// save() 호출 없이도 트랜잭션 커밋 시 UPDATE 발생
}
2. 명시적 save()를 호출하면 어떻게 되는가
Spring Data JPA의 save() 내부 구현(SimpleJpaRepository)을 보면 다음과 같이 동작합니다.
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity); // INSERT
} else {
return em.merge(entity); // 영속성 컨텍스트에 병합, UPDATE
}
}
isNew()로 신규 엔티티 여부를 판단해 persist()와 merge() 중 하나를 실행합니다.
이미 영속 상태인 엔티티에 save()를 호출하면?
같은 트랜잭션 내에서 조회한 엔티티는 이미 영속 상태(managed)입니다. 이 상태에서 save()를 호출하면 merge()가 실행되는데, 이미 같은 영속성 컨텍스트에 있는 엔티티를 merge하면 사실상 아무 추가 작업도 하지 않습니다. 결국 UPDATE는 Dirty Checking에 의해 동일하게 발생합니다.
@Transactional
public void updateUserName(Long userId, String newName) {
User user = userRepository.findById(userId).orElseThrow();
user.setName(newName);
userRepository.save(user); // 불필요한 코드 — Dirty Checking으로 동일하게 동작
}
즉, 이미 영속 상태인 엔티티에 대한 save() 호출은 불필요한 코드입니다.
3. 명시적 save()가 필요한 경우
새 엔티티를 저장할 때
가장 기본적인 케이스입니다. 비영속 상태의 엔티티를 처음 저장할 때는 반드시 save()를 호출해야 합니다.
User user = new User("승주"); // 비영속 상태
userRepository.save(user); // persist() → INSERT
준영속(detached) 엔티티를 다시 병합할 때
트랜잭션이 끝난 뒤 반환된 엔티티는 준영속 상태입니다. 이를 수정하고 새 트랜잭션에서 저장해야 하는 경우, save() → merge()가 의미 있습니다.
// 트랜잭션 1 종료 후 반환된 엔티티 (준영속 상태)
User detachedUser = userService.getUser(userId);
detachedUser.setName("새이름");
// 새 트랜잭션에서 저장
userRepository.save(detachedUser); // merge() → 영속성 컨텍스트에 복사 후 UPDATE
merge()는 전달받은 detached 엔티티의 상태를 영속성 컨텍스트에 복사한 새로운 managed 인스턴스를 반환합니다. 전달한 detached 인스턴스가 managed 상태로 바뀌는 것이 아니라는 점에 주의해야 합니다.
User managed = userRepository.save(detachedUser);
// detachedUser → 여전히 준영속
// managed → 새로운 영속 인스턴스
신규/기존 엔티티 구분이 모호할 때
save()가 isNew() 판단을 내부적으로 처리해주므로, 호출자가 INSERT/UPDATE를 직접 구분하지 않아도 됩니다.
4. 정리
상황 권장 방식 이유
| 같은 트랜잭션 내 조회 후 수정 | Dirty Checking | JPA 설계 의도에 맞고 불필요한 오버헤드 없음 |
| 새 엔티티 저장 | save() | persist() 호출 필요 |
| 준영속 엔티티 병합 | save() | merge()로 영속성 컨텍스트에 재등록 |
| INSERT/UPDATE 구분이 모호 | save() | isNew() 판단을 위임 |
같은 트랜잭션 내에서 조회 후 수정하는 패턴이라면 Dirty Checking에 맡기는 것이 JPA의 설계 의도에 맞습니다. save()는 새 엔티티 저장이나 detached 엔티티 병합처럼 실제로 필요한 상황에서만 사용하는 것이 바람직합니다.
'Java & Spring > JPA' 카테고리의 다른 글
| N+1문제와 Fetch 전략 - LAZY, Fetch Join, @BatchSize (0) | 2026.05.09 |
|---|---|
| 영속성 컨텍스트 메커니즘 - 1차 캐시 · Dirty Checking · 쓰기 지연 (1) | 2026.05.09 |
| Spring Data JPA는 내부적으로 어떻게 동작할까? (0) | 2026.04.09 |