JPA

JPA 더티 체킹(Dirty Checking)이란?

RyanGomdoriPooh 2019. 9. 14. 23:34

JPA(Java Persistence API)를 사용하면서 더티 체킹과 트랜잭션의 관계에 대해서 알고 있지 않으면,

비즈니스 로직에서 다루는 엔티티 데이터가 꼬이는 경우가 발생합니다.

 

데이터가 꼬이는 경우를 방지하려면,

더티 체킹(Dirty Checking)이 어떤 상황에 사용이 되는지 알고 있어야 합니다.

 

여기서는 더티 체킹이 어떤 상황에서 발생하는지 확인해보려고 합니다.

 

더티 체킹(Dirty Checking)이란?

  • JPA는 엔티티 매니저가 엔티티를 저장/조회/수정/삭제를 합니다.
  • 그런데 엔티티 매니저의 메서드를 찾아보면, 저장(persist)/조회(find)/수정(?)/삭제(delete)로 수정에 해당하는 메서드가 없습니다.
  • 대신에 수정(?)에 해당하는 더티 체킹(Dirty Checking)을 지원합니다.
  • 더티 체킹은 Transaction 안에서 엔티티의 변경이 일어나면, 변경 내용을 자동으로 데이터베이스에 반영하는 JPA 특징입니다.
  • *데이터베이스에 변경 데이터를 저장하는 시점 : 1) Transaction commit 시점 2) EntityManager flush 시점 3) JPQL 사용 시점
  • 또한, 영속성 컨택스트(Persistence Context) 안에 있는 엔티티를 대상으로 더티 체킹이 일어납니다.
  • 여기서 Dirty란 "엔티티 데이터의 변경된 부분"으로 해석하시면 됩니다. 즉, 변경된 부분을 체크해서 DB에 반영한다는 뜻으로 해석합니다.

JPA 영속성 컨텍스트

더티 체킹이 일어나는 환경은 아래 두 가지 조건이 충족되어야 합니다.

  • 영속 상태(Managed) 안에 있는 엔티티인 경우
  • Transaction 안에서 엔티티를 변경하는 경우

Transaction은 두 가지 방식으로 사용할 수 있습니다.

     1번. Service Layer에서 @Transactional을 사용하는 경우

     2번. EntityTransaction을 이용해서 트랜잭션을 범위를 지정하고 사용하는 경우

 

여기서 위 1번/2번 Transaction 사용 예를 보이겠습니다.

1번과 2번은 PK를 id로 가지고 있는 User의 name을 바꾸는 동일한 작업을 하겠습니다.

 

1번. Service Layer에서 @Transactional을 사용하는 경우

@Service
public class ExampleService {
     @Transactional
     public void updateInfo(Long id, String name) {
          User user = userRepository.findById(id);
          user.setName(name);
     }
}

2번. EntityTransaction을 이용해서 트랜잭션을 범위를 지정하고 사용하는 경우

// 2번에서는 자세하게 실제 작동 방식에 대해서 주석으로 설명하겠습니다.

@Service
public class ExampleService {
     public void updateInfo(Long id, String name) {
          EntityManager em = entityManagerFactory.createEntityManager();
          EntityTransaction tx = em.getTransaction();
          // 1. 트랜잭션 시작
          tx.begin();
          // 2.User 엔티티를 조회 & User 스냅샷 생성
          User user = em.find(User.class, id);
          // 3.User 엔티티의 name을 변경
          user.setName(name);
          // 4. 트랜잭션
          // 5.User 스냅샷과 최종 user의 내용을 비교해서 Dirty를 Checking 해서 Update Query 발생
          tx.commit();
     }
}

 

더티 체킹이 발생하면 어떤 절차가 진행될까요?

위 1번/2번에서 더티 체킹이 일어나면 다음과 같은 Query 문이 발생한 것을 볼 수 있습니다.

 

분명 user의 업데이트에 대한 메서드를 호출하지 않았음에도 불구하고 Query가 발생시킵니다.(더티 체킹)

 

그리고 트랜잭션 커밋 이후, 트랜잭션이 끝나는 시점에 모든 엔티티에 대한 정보를 DB에 반영합니다.

더티 체킹시, 발생하는 Query 예시)

Hibernate: 
update
  user
set
  name=?
where
  id=?

그럼 트랜잭션이 아닌 상태에서 엔티티 내용이 변경되면 어떻게 될까요?

Service Layer안, 동일한 코드 메서드에 @Transactional를 1) 사용한 경우, 2) 사용하지 않은 경우를 예시로 들겠습니다.

 

코드는 1), 2)가 다음과 같이 동일합니다.

public void updateInfo() { 
    User user = userRepository.findById(2L)
        .orElseThrow(() -> new ErrorCodeException(ErrorType.USER_IS_NOT_EXISTING));
    user.setEmail("hello@gmail.com");
    System.out.println(userRepository.existsByEmail("hello@gmail.com"));
}

위 코드로 어떻게 코드가 작동하는지 순서대로 보여드리겠습니다.

 

@Transactional를 사용한 경우

1. Table: User에서 PK id가 2인 user 엔티티 객체 조회
2. user의 email을 hello@gmail.com로 수정 (Dirty Checking 발동)
3. hello@gmail.com를 email로 가진 user 엔티티 유무 확인, 2번 과정에서 수정되었기에 true 출력

@Transactional를 사용하지 않은 경우

1. Table: User에서 PK id가 2인 user 엔티티 객체 조회
2. user의 email을 hello@gmail.com로 수정
3. hello@gmail.com를 email로 가진 user 엔티티 유무 확인,
2번 과정에서 수정되지 못해서 false 출력

Transaction을 사용하지 않아서 반영되지 못한 내용을 반영하고 싶은 경우,

원하는 시점에 save, saveAndFlush를 사용합니다.

public void updateInfo() { 
    User user = userRepository.findById(2L)
        .orElseThrow(() -> new ErrorCodeException(ErrorType.USER_IS_NOT_EXISTING));
    user.setEmail("hello@gmail.com");
    userRepository.saveAndFlush(user);
    System.out.println(userRepository.existsByEmail("hello@gmail.com"));
} 


1. Table: User에서 PK id가 2인 user 엔티티 객체 조회
2. user의 email을 hello@gmail.com로 수정
3. hello@gmail.com를 email로 가진 user 엔티티 유무 확인, 
이번엔 userRepository.saveAndFlush(user);를 통해서 반영이 되어 true 출력

이상으로 JPA의 더티 체킹(Dirty Checking)에 대해서 설명을 마치겠습니다~!