벌크 연산 할 때 ObjectOptimisticLockingFailureException 발생한 것 분석

dasd412·2022년 12월 28일
0

MSA 프로젝트

목록 보기
15/25
post-custom-banner

문제 상황

실행 되지 않는 코드

    @Transactional
    private void removeDiaryWithSubEntities(Long diaryId) throws TimeoutException {
		...
        
        foodRepository.deleteFoodsInIds(foodIds);
        dietRepository.deleteDietsInIds(dietIds);
        
        diaryRepository.deleteById(diaryId); //<- 되지 않는 코드
    }

이 코드의 deleteById()는 JPA 기본 제공 메서드이다.

간단한 로그

Hibernate: delete from food where food_id in (? , ? , ? , ? , ? , ? , ?)
Hibernate: delete from diet where diet_id in (? , ? , ? , ?)
Hibernate: delete from food where food_id=?

org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement@1628378220 wrapping prep19: delete from food where food_id=? {1: 1}; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: HikariProxyPreparedStatement@1628378220 wrapping prep19: delete from food where food_id=? {1: 1}

실행 되는 코드

    @Transactional
    private void removeDiaryWithSubEntities(Long diaryId) throws TimeoutException {
		
        ...
        
        foodRepository.deleteFoodsInIds(foodIds);
        dietRepository.deleteDietsInIds(dietIds);
        
        diaryRepository.deleteDiaryForBulkDelete(diaryId); //<- 되는 코드
    }

간단한 로그

Hibernate: delete from food where food_id in (? , ? , ? , ? , ? , ? , ?)
Hibernate: delete from diet where diet_id in (? , ? , ? , ?)
Hibernate: delete from diabetes_diary where diary_id=?

되는 코드

public class DiaryRepositoryImpl implements DiaryRepositoryCustom {

    private final JPAQueryFactory jpaQueryFactory;

    public DiaryRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }

    @Override
    public void deleteDiaryForBulkDelete(Long diaryId) {
        jpaQueryFactory.delete(QDiabetesDiary.diabetesDiary)
                .where(QDiabetesDiary.diabetesDiary.diaryId.eq(diaryId))
                .execute();
    }
}

Querdsl로 짠 코드이다.


엔티티 연관 관계

핵심 빼곤 전부 생략한 코드이다.

public class DiabetesDiary {
    @OneToMany(mappedBy = "diary", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private final List<Diet> dietList = new ArrayList<>();
}
public class Diet {
    @OneToMany(mappedBy = "diet", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private final List<Food> foodList = new ArrayList<>();
}
public class Food {

}

로그 레벨 높이기

properties를 사용한다면, 다음으로 바꾼다.

logging.level.org.hibernate=TRACE
logging.level.org.hibernate.hql=INFO

logback을 사용한다면, 다음으로 바꾼다.

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.hibernate" level="TRACE">
        <appender-ref ref="CONSOLE" />
    </logger>
    <logger name="org.hibernate.hql" level="INFO">
        <appender-ref ref="CONSOLE" />
    </logger>
</configuration>

분석

로그 레벨 Trace로 해도 영양가 있는 로그는 없었다.

낙관적 락과 비관적 락

낙관적 락이란 트랜잭션 충돌이 일어나지 않을 것이라 보고,
트랜잭션 커밋할 때 충돌이 발생하면 그 때 처리하자는 방식이다.

비관적 락이란 트랜잭션 충돌이 일어날 것이라 보고, 우선 락을 거는 방식이다.

아마도 원인

기본적으로 제공하는 deleteById(diaryId)는 상위 엔티티를 삭제할 때, cascadeType.all을 본다. 그래서 하위 엔티티도 삭제하게 된다. 그런데 이미 foodRepository.deleteFoodsInIds(foodIds); dietRepository.deleteDietsInIds(dietIds);
에서 하위 엔티티들이 전부 삭제되어 있었다. DB에도 해당 사항이 반영되어 있는 것이다.

그런데 cascadeType.all에 의해 하위 엔티티를 삭제해야 하는데, 삭제 이전에 무엇이 필요할까? 바로 조회다. 조회를 해야하는데 이미 DB에 삭제가 되어 있으니 조회가 되겠나. 그래서 오류가 난 것 같다.

그럼 Querydsl로 짠 코드는 왜 오류가 안 났을까. 내 생각엔 cascadeType.all을 시행하지 않고, 순수하게 엔티티 하나만 삭제하기 때문에 잘 작동하는 듯 하다.

참고로, @Transactional(isolation = Isolation.SERIALIZABLE)까지 격리수준을 높여봤는데도 동일한 오류가 발생했었다.

참고 링크

https://stackoverflow.com/questions/21625059/org-hibernate-stalestateexception-batch-update-returned-unexpected-row-count-fr


profile
SW 마에스트로 14기 (스타트업 재직중)
post-custom-banner

0개의 댓글