Spring Data JPA 더티 체킹(Dirty Checking)

Jake·2023년 5월 24일
0

Spring

목록 보기
1/2
post-thumbnail

Spring Data Jpa 와 같은 ORM 구현체를 사용하다보면 더티 체킹이라는 단어가 종종 보입니다.

모든 코드는 Github 에 있습니다

이번 시간에는 더티 체킹이 무엇인지 알아보겠습니다.

여기서 잠깐 !

Spring Data Jpa 와 JPA, Hibernate 차이

Spring Data Jpa = JPA를 쓰기 편하게 만들어 놓은 모듈, 사용자가 Repository 인터페이스에 정해진 규칙대로 메소드를 입력하면, Spring이 알아서 해당 메소드 이름에 적합한 쿼리를 날리는 구현체를 만들어서 Bean으로 등록해준다.

JPA = 기술명세, 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스

Hibernate = JPA의 구현체, interface와 이를 구현한 class와 같은 관계

더티체킹이란 ?

Dirty 란 상태의 변화가 생긴 정도로 이해하면 됩니다
즉, Dirty Checking 이란 상태 변경 검사입니다

먼저 Spring Data JPA를 보기 전에 JPA에서 엔티티 매니저를 사용하여 흔히 save()라고 하는 동작이 어떻게 수행되는지 알아보겠습니다

@RequiredArgsConstructor
@Service
public class PayService {

    public void updateNative(Long id, String tradeNo) {
        EntityManager em = entityManagerFactory.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin(); //트랜잭션 시작
        Pay pay = em.find(Pay.class, id);
        pay.changeTradeNo(tradeNo); // 엔티티만 변경
        tx.commit(); //트랜잭션 커밋
    }
}

위 코드는 별도의 save 없이
1. 트랜잭션이 시작되고
2. 엔티티를 조회하고
3. 엔티티의 값을 변경하고
4. 트랜잭션을 커밋합니다

여기에서는 데이터베이스에 update 쿼리에 관한 코드는 어디에도 없습니다.

여기서 아래 테스트코드를 실행한다면 어떤일이 발생할까요 ?

@Test
    public void 엔티티매니저로_확인() {
        //given
        Pay pay = payRepository.save(new Pay("test1",  100));

        //when
        String updateTradeNo = "test2";
        payService.updateNative(pay.getId(), updateTradeNo);

        //then
        Pay saved = payRepository.findAll().get(0);
        assertThat(saved.getTradeNo()).isEqualTo(updateTradeNo);
    }

실행해보면 update 쿼리가 나가는 것을 확인할 수 있습니다

save를 하지 않아도 Dirty Checking 덕분에 변경사항이 적용되었습니다.

JPA 에서는 이와 같이 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해줍니다.

이때 변화의 기준은 최초 조회 상태입니다.

JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어놓습니다.
그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른점이 있다면 Update Query를 데이터베이스로 전달합니다.

당연히 이런 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만적용 됩니다.

  • detach된 엔티티 (준영속)
  • DB에 반영되기 전 처음 생성된 엔티티 (비영속)

등 준영속/비영속 상태의 엔티티는 Dirty Checking 대상에 포함되지 않습니다.
즉, 값을 변경해도 데이터베이스에 반영되지 않습니다.



public void cancel(Long applyId) {
        Apply apply = applyRepository.findById(applyId).get();
        apply.cancelApply();
    }
    
public void cancelApply() {
        if (getStatus() == ApplyStatus.DONE) {
            throw new IllegalStateException("이미 팀 구성이 완료되었습니다");
        } else {
            System.out.println("CANCEL");
            this.setStatus(ApplyStatus.CANCEL); // Apply 상태를 CANCEL 로 변경
        }
    }

cancel() 은 apply의 속성인 applyStatus를 APPLY -> CANCEL 로 변경하는 로직입니다.

이 경우 update 쿼리가 발생할까요 ?

그렇지 않습니다. 단순히 값만 변경이 되었다고 해서 엔티티가 영속성 컨텍스트에 의해 관리되고 Dirty Checking이 매번 발생하는 것은 아닙니다.

저는 처음에 "applyStatus 값이 변경되었는데 왜 update 쿼리가 발생하지 않지?" 라는 의문을 가졌습니다.

이는 놓치기 쉬운 실수였습니다. 위 cancel() 가 실행하는 동작은 데이터베이스의 값들을 다루는 하나의 Transaction 입니다.

Transaction을 다룰때는 @Transactional 어노테이션을 붙여주는 것이 좋습니다. 그래야 영속성 컨텍스트에 의해 관리되기 때문인 것 같습니다.

또한, 이 부분을 해주어야 트랜잭션의 4가지의 성질이 지켜집니다.

원자성 : 한 트랜잭션 내에서 실행한 작업들은 하나의 단위로 처리
일관성 : 트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다
격리성 : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리한다
영속성 : 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 한다

@Transactional
public void cancel(Long applyId) {
        Apply apply = applyRepository.findById(applyId).get();
        apply.cancelApply();
    }

과 같이 어노테이션을 붙여주니 아래처럼 update 쿼리가 잘 발생하였습니다.

여기서 만약 변경된 부분만 update 쿼리를 발생시키고 싶다면

@Entity
@DynamicUpdate // 변경한 필드만 대응
@Getter @Setter
public class Apply {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "apply_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @Column(name = "apply_time")
    private LocalDateTime applyTime; // 신청 시간

    @Column(name = "apply_status")
    @Enumerated(EnumType.STRING)
    private ApplyStatus status; // 신청 상태 [APPLY, CANCEL, PROCESS, DONE]

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "project_id")
    private Project project;

과 같이 엔티티에 @DynamicUpdate 어노테이션을 붙여주게 된다면 아래와 같이 변경 필드만 반영되도록 할 수 있습니다

변경분(apply_status)만 Update 쿼리에 반영된 것을 확인할 수 있습니다.

0개의 댓글