[JPA] JPA의 update 방식

주재완·2025년 3월 18일
0
post-thumbnail

코드 리뷰를 하면서 다음과 같은 피드백을 받았습니다.

update 할 때 Dirty Checking 방식을 사용하는 것이 유지보수에 좋아요!

이를 정리하기 위해 블로그 포스팅을 남기게 되었습니다.

1. 기존의 코드

초기에 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차 캐시가 갱신되지 않아서, 이후 동일한 엔티티를 조회할 경우 데이터 불일치 문제가 발생할 수 있습니다.

또한, 코드 가독성이 떨어지고 유지보수성이 낮습니다. 그리고 트랜잭션 롤백이 발생해도 이미 실행된 쿼리는 취소되지 않습니다.

2. Dirty Checking

위 문제를 해결하기 위해 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 메서드가 필요하지 않습니다.
  • JPA는 영속성 컨텍스트 내에서 엔티티가 변경되었음을 감지하고 자동으로 UPDATE 쿼리를 실행하기 때문에 직접 명시하지 않아도 됩니다.

엔티티 코드

@Entity
public class Town {
    
    public void updateTownName(String townName) {
        this.name = townName;
    }
}
  • townName 필드의 값을 변경하면 JPA는 이를 감지하고, 트랜잭션이 종료될 때 해당 엔티티의 변경 사항을 반영하는 UPDATE 쿼리를 실행합니다.

변경된 코드의 특징

엔티티를 조회한 후 필드 값을 변경하면, 트랜잭션 종료 시점에 자동으로 UPDATE 쿼리가 실행됩니다. 여기서 영속성 컨텍스트가 유지되므로 데이터 불일치 문제가 발생하지 않습니다.

그리고 트랜잭션이 롤백될 경우, 변경 사항도 함께 롤백됩니다. 마지막으로 코드가 간결해지고 유지보수가 쉬워집니다.

JPA의 Dirty Checking 동작 원리

JPA는 영속성 컨텍스트에서 엔티티를 관리하며, 엔티티의 스냅샷(초기 상태)을 저장합니다.

이후 트랜잭션이 종료될 때 스냅샷과 현재 엔티티의 상태를 비교하여 변경된 경우 UPDATE 쿼리를 자동 실행합니다. 따라서 별도의 save() 호출 없이도 변경 사항이 자동 반영됩니다.

3. 각각의 장단점

지금까지 보았을 때 Dirty Checking 방식이 무조건 좋아보이지만, 각각의 장단점이 존재합니다.

Bulk Update 방식

특징

  • @Modifying을 활용하여 직접 SQL 실행
  • 즉시 반영되지만 영속성 컨텍스트와는 무관

장점

  • 대량 데이터 업데이트 시 성능이 뛰어남
  • 한 번의 SQL 실행으로 여러 행을 업데이트 가능

단점

  • 영속성 컨텍스트 미반영 문제
  • 트랜잭션 롤백이 되지 않음
  • 코드가 복잡해지고 유지보수가 어려워짐

Dirty Checking 방식

특징

  • 엔티티를 조회한 후 필드 값을 변경하면 트랜잭션 종료 시점에 자동 반영
  • 영속성 컨텍스트를 활용하여 변경 사항을 감지

장점

  • 데이터 일관성이 보장(영속성 컨텍스트 활용)
  • 트랜잭션 롤백이 쉬움
  • 코드 유지보수성이 높아짐

단점

  • 대량 업데이트 시 개별 쿼리가 발생하여 성능이 저하될 수 있음
  • 엔티티 조회 후 변경하는 과정이 필요

4. 결론

Bulk Update 방식은 대량 데이터를 빠르게 업데이트할 때 유리하지만, 영속성 컨텍스트를 무시하기 때문에 데이터 일관성이 깨질 위험이 있습니다. 반면 Dirty Checking 방식은 JPA의 변경 감지를 활용하여 코드 유지보수성이 높고, 트랜잭션을 활용할 수 있어 더 안정적입니다.

어떤거 선택해야 되나요?

  • 대량 업데이트가 필요한 경우: Bulk Update 방식이 유리
  • 트랜잭션 내에서 엔티티를 변경하는 경우: Dirty Checking 방식이 적합
profile
안녕하세요! 언제나 탐구하고 공부하는 개발자, 주재완입니다.

0개의 댓글