본문 바로가기

spring

더티 체킹(Dirty Checking) - 개념 및 주의사항 / @DynamicUpdate

더티 체킹(Dirty Checking)이란?
: JPA가 트랜잭션 커밋 시점에 영속성 컨텍스트에 저장된 엔티티 객체의 상태 변경 여부를 감지하여, 자동으로 DB에 반영해주는 기능

 

 

언제 발생할까?

 

1. 트랜잭션 안에서

 

2. 영속 상태(EntityManager가 관리 중)인 엔티티에 대해

 

3. save() 등의 메서드 없이 필드를 변경만 하고

 

4. 트랜잭션이 commit될 때

 

→ JPA가 내부적으로 flush()를 수행, 이전 스냅샷과 비교해서 변경이 감지되면 UPDATE 쿼리를 날린다 !

flush()란?
: 영속성 컨텍스트의 변경 내용을 DB에 반영하는 동작
→ 트랜잭션 commit 직전에 JPA가 자동으로 호출, 이 시점에 더티 체킹이 수행됨

 

예)

@Transactional
public void updateUser(Long id, String newName) {
    User user = userRepository.findById(id)
                              .orElseThrow(() -> new RuntimeException("User not found"));
    user.setName(newName);  // 변경만 했을 뿐 save() 호출 안 함
    // 트랜잭션 종료 시점에 Dirty Checking → UPDATE 쿼리 자동 실행
}

→ 명시적인 DB 저장 호출 없이도 update 쿼리가 실행되어 변경이 반영됨

 

 

어떻게 동작할까?

 

1. 트랜잭션 시작 시, JPA는 user 객체를 영속성 컨텍스트에 넣고 "스냅샷"을 저장

 

2. user.setName("새 이름")으로 필드 값을 변경

 

3. 트랜잭션 커밋 시점에 flush → 스냅샷과 비교 (최초 조회 상태와 비교하여 변화가 있는지 감지)

 

4. 차이가 있으면 : UPDATE 쿼리 실행

 

 

⚠️ 유의할 점

 

1. @Transactional이 없으면 더티 체킹이 동작하지 않는다 !

@Transactional
public FaceIssueReportResponse findFaceIssueReport(Long id){
    FaceIssueReport faceIssueReport = faceIssueReportRepository.findById(id)
            .orElseThrow(() -> new CustomException(ErrorCode.FACE_ISSUE_REPORT_NOT_FOUND));

    faceIssueReport.setRead(true);  // 값이 변경되어 dirty checking 대상이 됨
    return faceIssueReportMapper.toResponseDto(faceIssueReport); // DB 반영됨
}
  • JPA는 트랜잭션 범위 내에서만 변경 감지를 수행하기 때문에 변경을 감지하려면 트랜잭션이 있어야 함
  • DB를 다루는 메서드이기 때문에 하나의 트랜잭션으로 처리하기

 

2. 기본적으로 모든 필드가 UPDATE 쿼리에 포함된다

 

더티 체킹을 통해 자동으로 update 쿼리가 생성되면 기본적으로 모든 필드를 업데이트함

→ 실제 변경하려는 필드에만 update 쿼리를 발생시키려면 엔티티에 @DynamicUpdate 어노테이션을 붙이기 !

@Entity
...
@DynamicUpdate
public class FaceIssueReport extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    private boolean isRead;
}

 

엔티티에 @DynamicUpdate 어노테이션을 붙인 뒤 실행 시,

 

변경된 필드만 update 쿼리가 실행됨을 확인할 수 있음

 

updatedAt 필드는 @DynamicUpdate 어노테이션과 관계 없이 update 쿼리에 포함되는 이유
: updatedAt 필드는 JPA Auditing 기능을 통해 자동으로 값이 변경되도록 설정된 상태
→ @LastModifiedDate가 붙은 필드는 자동 필드 변경이 일어남
→ updatedAt도 수정된 필드로 간주하여 더티 체킹에서의 update 쿼리에 포함시킴 !
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

 

참고)
트랜잭션 시작 시 조회한 스냅샷과의 비교를 통해 변화를 감지하므로,

이미 isRead가 true인 엔티티에 대해서는 해당 메서드를 실행하더라도 update 쿼리가 생성되지 않음

 

 

❗ @DynamicUpdate 사용의 단점

 

@DynamicUpdate를 사용하면 변경된 필드만 업데이트하므로 업데이트 성능이 개선될 것 같지만, 항상 좋은 것은 아니다 !

 

1. SQL 쿼리 캐싱 비효율

  • JPA는 기본적으로 쿼리를 캐싱하여 성능을 최적화하지만,
  • @DynamicUpdate를 사용하면 런타임 시점에 실제로 변경된 필드를 기준으로 쿼리를 동적으로 생성
  • SQL 캐시 히트율이 떨어져 결과적으로 성능 저하 가능성 존재

2. JPA에서 엔티티 객체의 변경이 아닌 필드 수준의 추적이 필요

 

3. JPA 표준이 아닌 Hibernate에 종속적인 어노테이션

 

 

언제 사용하는 게 좋을까?

→ 필드 수가 많은 엔티티에서
→ 실질적으로 변경되는 필드는 적은 경우

...

성능 이슈나 유지보수 측면에서 과도한 사용을 피하고, 필요한 엔티티에만 제한적으로 적용하는 것이 바람직함