JPA Dirty Checking
- 코드에서 데이터베이스에 엔티티를 update하는 쿼리가 존재하지 않는데, entity의 값을 변경하는 것 만으로도 데이터베이스의 데이터가 변경됨
- 이는 JPA의 Dirty Checking 때문이며, Dirty Checking은 entity의 상태 변경을 검사하는 것을 의미함
- JPA에서 트랜잭션이 끝나는 시점에 변화가 있는 모든 entity 객체를 데이터베이스에 자동으로 반영함
- JPA는 commit하는 순간 flush를 호출하고, 이때 entity와 snapshot을 비교함
- 영속성 컨텍스트에 처음 저장된 순간 entity의 최초 상태를 1차 캐시에 저장함 (snapshot)
- commit하는 순간 entity와 snapshot을 비교하여 변경된 값이 있는지 확인하고, 변경된 값이 있다면 update 쿼리를 쓰기 지연 SQL에 저장함
- 영속 상태가 아닐 경우, 값을 변경해도 데이터베이스에 반영되지 않음
JPA Dirty Checking 테스트 코드
PharmacyRepositoryService.java
package com.example.pharmacy_navigation.pharmacy.service;
import com.example.pharmacy_navigation.pharmacy.entity.Pharmacy;
import com.example.pharmacy_navigation.pharmacy.repository.PharmacyRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class PharmacyRepositoryService {
private final PharmacyRepository pharmacyRepository;
@Transactional
public void updateAddress(Long id, String address){
Pharmacy entity = pharmacyRepository.findById(id).orElse(null);
if(Objects.isNull(entity)){
log.error("[PharmacyRepositoryService updateAddress] not found id: {}", id);
return;
}
entity.changePharmacyAddress(address);
}
// for test (Transactional annotation 없이 실행)
public void updateAddressWithoutTransaction(Long id, String address){
Pharmacy entity = pharmacyRepository.findById(id).orElse(null);
if(Objects.isNull(entity)){
log.error("[PharmacyRepositoryService updateAddress] not found id: {}", id);
return;
}
entity.changePharmacyAddress(address);
}
}
- updateAddress( ): Transactional annotation을 적용했기 때문에 entity의 상태 변화가 일어남. Dirty Checking이 성공할 것으로 예상
- updateAddressWithoutTransaction( ): Transactional annotation을 적용했기 때문에 entity의 상태 변화가 일어나지 않음. Dirty Checking이 실패할 것으로 예상
PharmacyRepositoryServiceTest.groovy
package com.example.pharmacy_navigation.pharmacy.service
import com.example.pharmacy_navigation.AbstractIntegrationContainerBaseTest
import com.example.pharmacy_navigation.pharmacy.entity.Pharmacy
import com.example.pharmacy_navigation.pharmacy.repository.PharmacyRepository
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
class PharmacyRepositoryServiceTest extends AbstractIntegrationContainerBaseTest {
@Autowired
private PharmacyRepositoryService pharmacyRepositoryService
@Autowired
private PharmacyRepository pharmacyRepository
def setup() {
pharmacyRepository.deleteAll()
}
def "PharmacyRepository update - dirty checking success"() {
given:
String inputAddress = "서울 특별시 성북구 종암동"
String modifiedAddress = "서울 광진구 구의동"
String name = "은해 약국"
def pharmacy = Pharmacy.builder()
.pharmacyAddress(inputAddress)
.pharmacyName(name)
.build()
when:
def entity = pharmacyRepository.save(pharmacy)
pharmacyRepositoryService.updateAddress(entity.getId(), modifiedAddress)
def result = pharmacyRepository.findAll()
then:
result.get(0).getPharmacyAddress() == modifiedAddress
}
def "PharmacyRepository update - dirty checking fail"() {
given:
String inputAddress = "서울 특별시 성북구 종암동"
String modifiedAddress = "서울 광진구 구의동"
String name = "은해 약국"
def pharmacy = Pharmacy.builder()
.pharmacyAddress(inputAddress)
.pharmacyName(name)
.build()
when:
def entity = pharmacyRepository.save(pharmacy)
pharmacyRepositoryService.updateAddressWithoutTransaction(entity.getId(), modifiedAddress)
def result = pharmacyRepository.findAll()
then:
result.get(0).getPharmacyAddress() == inputAddress
}
}
테스트 코드 실행 결과
PharmacyRepository update - dirty checking success 테스트 실행 결과
- update 구문이 실행된 것을 확인함
- Dirty Checking 성공
PharmacyRepository update - dirty checking fail 테스트 실행 결과