코드 리뷰를 하면서 다음과 같은 피드백을 받았습니다.
update 할 때 Dirty Checking 방식을 사용하는 것이 유지보수에 좋아요!
이를 정리하기 위해 블로그 포스팅을 남기게 되었습니다.
초기에 JPA의 @Modifying과 @Query를 사용하여 Bulk Update 방식으로 데이터를 업데이트하는 코드를 작성했습니다.
@Service
@RequiredArgsConstructor
@Transactional
public class TownService {
private final TownRepository townRepository;
public void updateTownName(User user, TownNameUpdateDto townNameUpdateDto) {
int updatedCount = townRepository.updateTownName(user.getId(), townNameUpdateDto.townName());
if(updatedCount == 0) {
throw new InvalidArgumentException("해당하는 마을을 찾을 수 없습니다.");
}
}
}
public interface TownRepository extends JpaRepository<Town, Long> {
@Modifying(clearAutomatically = true)
@Query("UPDATE Town t SET t.name = :townName WHERE t.id = :townId")
int updateTownName(Long townId, String townName);
}
@Modifying을 사용하여 직접 SQL UPDATE 쿼리를 실행하기 때문에 영속성 컨텍스트와 무관하게 DB가 변경됩니다. 따라서 1차 캐시가 갱신되지 않아서, 이후 동일한 엔티티를 조회할 경우 데이터 불일치 문제가 발생할 수 있습니다.
또한, 코드 가독성이 떨어지고 유지보수성이 낮습니다. 그리고 트랜잭션 롤백이 발생해도 이미 실행된 쿼리는 취소되지 않습니다.
위 문제를 해결하기 위해 Dirty Checking 방식으로 변경했습니다. JPA의 영속성 컨텍스트를 활용하여 엔티티의 필드 값이 변경되면 트랜잭션이 종료될 때 자동으로 업데이트 쿼리가 실행됩니다.
@Service
@RequiredArgsConstructor
@Transactional
public class TownService {
private final TownRepository townRepository;
public void updateTownName(User user, TownNameUpdateRequest townNameUpdateRequest) {
user.getTown().updateTownName(townNameUpdateRequest.townName());
}
}
townRepository.updateTownName()을 호출하여 직접 쿼리를 실행했던 방식에서 user.getTown().updateTownName()으로 변경되었습니다.@Modifying을 사용하여 즉시 쿼리를 실행했지만, Dirty Checking 방식에서는 엔티티의 상태를 변경하는 것만으로도 변경 사항이 자동 반영됩니다.public interface TownRepository extends JpaRepository<Town, Long> {
}
updateTownName()과 같은 명시적인 @Query 메서드가 필요하지 않습니다.UPDATE 쿼리를 실행하기 때문에 직접 명시하지 않아도 됩니다.@Entity
public class Town {
public void updateTownName(String townName) {
this.name = townName;
}
}
townName 필드의 값을 변경하면 JPA는 이를 감지하고, 트랜잭션이 종료될 때 해당 엔티티의 변경 사항을 반영하는 UPDATE 쿼리를 실행합니다.엔티티를 조회한 후 필드 값을 변경하면, 트랜잭션 종료 시점에 자동으로 UPDATE 쿼리가 실행됩니다. 여기서 영속성 컨텍스트가 유지되므로 데이터 불일치 문제가 발생하지 않습니다.
그리고 트랜잭션이 롤백될 경우, 변경 사항도 함께 롤백됩니다. 마지막으로 코드가 간결해지고 유지보수가 쉬워집니다.
JPA는 영속성 컨텍스트에서 엔티티를 관리하며, 엔티티의 스냅샷(초기 상태)을 저장합니다.
이후 트랜잭션이 종료될 때 스냅샷과 현재 엔티티의 상태를 비교하여 변경된 경우 UPDATE 쿼리를 자동 실행합니다. 따라서 별도의 save() 호출 없이도 변경 사항이 자동 반영됩니다.
지금까지 보았을 때 Dirty Checking 방식이 무조건 좋아보이지만, 각각의 장단점이 존재합니다.
@Modifying을 활용하여 직접 SQL 실행Bulk Update 방식은 대량 데이터를 빠르게 업데이트할 때 유리하지만, 영속성 컨텍스트를 무시하기 때문에 데이터 일관성이 깨질 위험이 있습니다. 반면 Dirty Checking 방식은 JPA의 변경 감지를 활용하여 코드 유지보수성이 높고, 트랜잭션을 활용할 수 있어 더 안정적입니다.