더티체킹은 Transaction 안에서 엔티티의 변경이 일어나면, 변경 내용을 자동으로 데이터베이스에 반영하는 JPA 특징이다.
Dirty Checking의 뜻은 변경 감지 정도로 생각할 수 있다. 즉, 변경을 감지해서 DB에 반영한다.
예를 들어 다음과 같은 코드가 있다.
@Slf4j
@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 하지 않는다.
여기서 데이터베이스에 update 쿼리에 관한 코드는 어디에도 없다.
이 코드가 어떻게 작동하는지 테스트 코드를 작성하여 수행해보면,
@RunWith(SpringRunner.class)
@SpringBootTest
public class PayServiceTest {
@Autowired
PayRepository payRepository;
@Autowired
PayService payService;
@After
public void tearDown() throws Exception {
payRepository.deleteAll();
}
@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);
}
}
아래와 같은 로그를 확인할 수 있다.
save 메소드로 변경 사항을 저장하지 않았음에도 update 쿼리가 실행되었다.
JPA에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.
이때 변화가 있다의 기준은 최초 조회 상태이다.
JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷을 만들어놓는다.
그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른점이 있다면 Update Query를 데이터베이스로 전달한다.
이런 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용 된다.
detach된 엔티티 (준영속)
DB에 반영되기 전 처음 생성된 엔티티 (비영속)
준영속/비영속 상태의 엔티티는 Dirty Checking 대상에 포함되지 않기 때문에 값을 변경해도 데이터베이스에 반영되지 않는다.
Spring Data Jpa와 @Transactional이 함께 할 경우엔 다음과 같다.
@Slf4j
@RequiredArgsConstructor
@Service
public class PayService {
private final PayRepository payRepository;
@Transactional
public void update(Long id, String tradeNo) {
Pay pay = payRepository.getOne(id);
pay.changeTradeNo(tradeNo);
}
}
위 코드의 테스트 코드를 작성해서 실행해보면
@Test
public void SpringDataJpa로_확인() {
//given
Pay pay = payRepository.save(new Pay("test1", 100));
//when
String updateTradeNo = "test2";
payService.update(pay.getId(), updateTradeNo);
//then
Pay saved = payRepository.findAll().get(0);
assertThat(saved.getTradeNo()).isEqualTo(updateTradeNo);
}
아래와 같이 정상적으로 update 쿼리가 수행됨을 확인할 수 있다.
Dirty Checking으로 생성되는 update 쿼리는 기본적으로 모든 필드를 업데이트한다.
JPA에서는 전체 필드를 업데이트하는 방식을 기본값으로 사용하며,
전체 필드를 업데이트하는 방식의 장점은 다음과 같다.
출처: 김영한님의 자바 ORM 표준 JPA 프로그래밍
다만, 필드가 20~30개 이상으로 많은 경우엔 이런 전체 필드 Update 쿼리가 부담스러울 수 있다.
(사실 이런 경우 정규화가 잘못된 경우일 확률이 높다.
한 테이블에 필드 30개는 확실히 많다.)
그래서 이런 경우엔 @DynamicUpdate로 변경 필드만 반영되도록 할 수 있다.
엔티티 최상단에 아래와 같이 @DynamicUpdate 를 선언해주시면 된다.
@Getter
@NoArgsConstructor
@Entity
@DynamicUpdate // 변경한 필드만 대응
public class Pay {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String tradeNo;
private long amount;
그리고 다시 테스트 코드를 수행해서 로그를 확인해보면!
변경분 (trade_no)만 Update 쿼리에 반영된 것을 확인할 수 있다.
Referrence
더티 체킹 (Dirty Checking)이란? by 향로