@Modifying과 더티체킹

Kevin·2025년 7월 13일

JPA

목록 보기
14/14
post-thumbnail

서론

최근 Spring boot + JPA(Hibernate) 환경에서 개발을 할 때 더티 체킹이 발생 하지 않는 문제가 발생 했다.

OSIV 옵션의 문제인가 싶었는데 해당 문제도 아니었기에 어떠한 문제인지를 알아내는데 어려움을 겪었다.

이 때 문제 발생했던 비즈니스 로직을 각색한 코드는 아래와 같다.

@Transactional
public void modifyMember(MemberUpdateRequest request) {
    affiliationRepository.deactivateByAccountId(account.getId());

    for (MemberUpdateRequest.OrganizationInfo orgInfo : request.getOrganizationList()) {
        Organization organization = organizationRepository.findByUuid(orgInfo.getOrganizationId())
                .orElseThrow(() -> new IllegalArgumentException("해당 조직을 찾을 수 없습니다."));

        Affiliation affiliation = affiliationRepository.findByAccountIdAndOrganizationId(account.getId(), organization.getId())
                .orElseThrow(() -> new IllegalStateException("소속 정보를 찾을 수 없습니다."));

        affiliation.activate();
    }
}

위 코드에서 deactivateByAccountId() 메서드를 구현한 Repository의 코드는 아래와 같다.

@Repository
public interface AffiliationRepository extends JpaRepository<Affiliation, Long> {

    @Modifying
    @Query("UPDATE Affiliation a SET a.active = false WHERE a.account.id = :accountId")
    void deactivateByAccountId(@Param("accountId") Long accountId);
}

@Modifying

위 코드에서 @Modifying 어노테이션에 집중을 해보자.

@Modifying 어노테이션이란 @Query 어노테이션을 통해 작성된 변경이 일어나는 쿼리(Insert, Delete, Update)를 실행할 때 사용한다.

@Modifying 어노테이션은 벌크성 수정, 삭제시에 사용되는데 벌크 연산 이란 데이터베이스에서 Insert, Delete, Update시 대량의 데이터를 한 번에 처리 하기 위한 작업을 의미한다.

이러한 벌크성의 @Modifying 쿼리는 영속성 컨텍스트를 무시하고 바로 DB에 적용되며,
해당 엔티티와 관련된 영속성 컨텍스트는 동기화되지 않는다.

그리고 이러한 특성으로 인해서 일괄 처리를 할 경우 효율적인 성능 개선을 얻을 수 있게 된다.

동기화 되지 않은 이유로는 EntityManager가 캐싱하고 있는 엔티티와 실제 DB의 상태가 불일치 하게 되기 때문이다.

이제 위에서 affiliation.activate()를 통한 더티 체킹이 왜 동작 하지 않았는지 이야기 해보자.


문제 원인

@Repository
public interface AffiliationRepository extends JpaRepository<Affiliation, Long> {

    @Modifying
    @Query("UPDATE Affiliation a SET a.active = false WHERE a.account.id = :accountId")
    void deactivateByAccountId(@Param("accountId") Long accountId);
}

위 벌크성 수정 쿼리에서는 영속성 컨텍스트를 무시하고 바로 DB에 반영이 된다.

즉 이로 인해서 JPA가 관리하는 1차 캐시(영속성 컨텍스트)에는 반영되지 않고, DB에만 직접 반영 되게 된다.

이로 인해서 이후에 영속성 컨텍스트에 남아있는 동일한 엔티티는 여전히 이전 상태로 남아있게 된다.

따라서 affiliation.activate()를 호출 해도, JPA 입장에서는 변경 된 게 없다고 판단을 해서 update 쿼리를 날리지 않게 된다.


해결 방법

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Documented
public @interface Modifying {
    boolean flushAutomatically() default false;

    boolean clearAutomatically() default false;
}

이 어노테이션에는 2가지 옵션이 존재한다.

flushAutomatically, clearAutomatically 옵션이 있다.

flushAutomatically 옵션은 기본 값은 false이며, true일 경우에 해당 쿼리를 실행 하기 이전에 영속성 컨텍스트의 변경 사항을 DB에 flush 한다.

clearAutomatically 옵션은 기본 값은 false이며, true일 경우 해당 쿼리를 실행 한 후, 영속성 컨텍스트를 clear한다.

이 두 옵션 중 clearAutomatically 어노테이션을 통해서 이 문제를 해결 할 수 있다.

@Modifying(clearAutomatically = true)
@Query("UPDATE Affiliation a SET a.active = false WHERE a.account.id = :accountId")
void deactivateByAccountId(@Param("accountId") Long accountId);

위 코드를 통해서 벌크 연산 이후 영속성 컨텍스트를 자동으로 clear 하여 해당 엔티티들을 모두 detach 상태로 만들고, 이후 findByXXX() 메서드들을 통해서 DB에서 최신 상태를 다시 조회 하여 1차 캐시에 저장 시킬 수 있다.

그리고 이를 통해서 더티 체킹이 동작 되도록 할 수 있다.

profile
Hello, World! \n

0개의 댓글