- 공부를 하는 과정에서 Spring Data Jpa와 같은 ORM 구현체를 사용하다보면 더티 체킹(Dirty Checking)이란 단어를 종종 듣게 됩니다.
- 당시에는 짧은 설명만 듣고 나름대로 이해했다고 생각하여 무심코 넘겼지만, 이번에 위 기능이 정확하게 어떻게 동작하는지 확실하게 정리해보고자 합니다.
💡 더티 체킹(Dirty Checking)
- 정확한 동작 방식을 설명하기 위해 Spring Data JPA가 아닌 네이티브한 코드를 살펴보겠습니다.
@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에서는 트랜잭션이 끝나는 시점에 변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해주는데, 이를 더티 체킹(Dirty Checking)이라고 합니다.
- 이때 Dirty란 "상태의 변화가 생긴 정도"로 이해할 수 있으며, 즉 Dirty Checking이란 상태 변경 검사를 의미합니다.
💡 더티 체킹의 기준
- 더티 체킹에서 "변화가 있다"의 기준은 최초 조회 상태입니다.
- JPA에서는 엔티티를 조회하면 해당 엔티티의 조회 상태 그대로 스냅샷(Snapshot)을 만들어 놓습니다. 그리고 트랜잭션이 끝나는 시점에는 이 스냅샷과 비교해서 다른점이 있다면 Update Query를 데이터베이스로 전달합니다.
- 당연히 이러한 상태 변경 검사의 대상은 영속성 컨텍스트가 관리하는 엔티티에만 적용됩니다.
- 즉, Detach된 엔티티(준영속), DB에 반영되기 전 처음 생성된 엔티티(비영속) 등 준영속/비영속 상태의 엔티티는 더티 체킹 대상에서 제외됩니다. 따라서 값을 변경해도 데이터베이스에 반영되지 않는 것이죠.
💡 Spring Data JPA의 더티 체킹
- Spring Data JPA에서는
@Transactional
을 사용하여 더티 체킹을 수행할 수 있습니다. - 아래 코드는 위의 네이티브 코드를 Spring Data JPA를 활용하여 작성한 코드입니다.
@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 쿼리가 수행됨을 확인할 수 있습니다.
💡 변경 부분만 update하고 싶을땐?
- JPA에서는 전체 필드를 업데이트하는 방식을 기본값으로 사용하기 때문에, 더티 체킹으로 생성되는 update 쿼리는 기본적으로 모든 필드를 업데이트합니다.
- 전체 필드를 업데이트하는 방식의 장점은 다음과 같습니다.
- 생성되는 쿼리가 같아 부트 실행시점에 미리 만들어서 재사용 가능하다.
- 데이터베이스 입장에서 쿼리의 재사용이 가능하다. (동일한 쿼리를 받으면 이전에 파싱된 쿼리를 재사용)
- 다만, 필드의 수가 많은 경우(20 ~ 30개 이상), 이러한 전체 필드 Update 쿼리가 부담스러울 수 있습니다.
사실 이런 경우 정규화가 잘못된 경우일 확률이 높습니다.
- 그래서 이런 경우엔
@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 쿼리에 반영된 것을 확인할 수 있습니다.
📌 Reference
'🥑 Web Technoloy' 카테고리의 다른 글
Vue.js & Spring Boot 연동 및 개발환경 구축 (1) | 2023.06.30 |
---|---|
@RequestBody vs @ModelAttribute (0) | 2023.06.24 |
AJAX란? (0) | 2023.06.13 |
RESTful API란? (0) | 2023.06.13 |
CSRF란? (0) | 2023.06.11 |