[JPA] org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1

호성·2022년 2월 13일
1
post-thumbnail

아래 에러는 왜 났을까?

org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
  • 단순히 A Entity를 지우는 api 요청을 했는데, 위와 같은 에러를 뱉는다.
  • A Entity는 B Entity와 연관 관계를 맺고 있다.
  • row수가 1개로 기대되나, 실제론 0개란다.

코드 상황

  • Product Entity
@OneToMany(cascade = CascadeType.ALL, mappedBy = "product")
private List<PhotoFile> photoFile;


  • PhotoFile Entity
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name ="product_id")
private Product product;

게시물(Product)Entity와 파일(PhotoFile) Entity는 위와 같이 양방향 연관관계를 맺고 있다.

  • 비즈니스 로직
@Transactional
public void delete(String userPhone, Long id) throws UserNotAuthorizedException {
        User user = userRepository.findByPhoneElseThrow(userPhone);
        Product product = productRepository.findByIdOrElseThrow(id);

        if(!isUserAuthor(user, product)) {
            throw new UserNotAuthorizedException("Current user and product author is not same.");
        }else {
            fileService.clearFileFromProduct(prod);
            productRepository.deleteById(id);
        }
}

User가 해당 게시물의 작성자가 아니라면 Exception을 throw하고, 맞다면 게시물을 지우는 비즈니스 로직을 수행한다.

@Transactional
public void clearFileFromProduct(Product product) {
List<PhotoFile> photoFile = product.getPhotoFile();

    if (photoFile.size() > 0) {
         photoFile.forEach(f -> s3UploaderService.deleteS3(f.getHashFilename()));
         fileRepository.deleteRelatedProductId(product.getId());
    }
}

위 부분을 통해 게시물 내에 사진을 Database에서 지우는 작업을 수행한다. 위 메소드가 호출되고 사진 데이터가 지워지고 나면, 게시물 데이터를 지운다.

처음 부터 차근 차근 살펴보자.

Product product = productRepository.findByIdOrElseThrow(id);

먼저, 위 코드를 통해 Product가 영속성 컨텍스트에 1차 캐싱되고,

일대다 관계인 PhotoFile은 기본적으로 LAZY Loading 되므로

photoFile.forEach(f -> s3UploaderService.deleteS3(f.getHashFilename()));

clearFileFromProduct 메소드 내에서 위 코드가 실행되는 시점에 1차 캐싱될 것이다.

fileRepository.deleteRelatedProductId(product.getId());

이후에 위 코드를 통해 PhotoFile가 삭제된다. 이 때, Product가 가지는 값은 아무것도 변경된 것이 없으므로 처음 1차 캐싱된 상태 그대로를 유지한다.

productRepository.deleteById(id);

위 메소드가 실행되면서 Product를 제거할 때, Product는 변경된 것이 없으므로 여전히 PhotoFile에 대한 참조를 가지고 있고, Product정의에서 PhotoFile에 대해 CascadeType.ALL로 정의되어 있으므로 Product를 영속성 컨텍스트에서 제거하면서 Product가 가지고 있는 PhotoFile한테 영속 상태를 전이하려고 한다. 즉, 영속성 컨텍스트에서 제거하려고 한다.

하지만 Product Entity가 참조하고 있던 PhotoFile은 이미 위에서 제거가 됐다.

actual row count: 0; expected: 1

따라서 제거하려는 엔티티가 실제로 존재하지 않을 때 위와 같은 에러가 발생하는 것이다.

이는 양방향 참조 관계를 명확하게 인지하고 있지 않아서 발생한 문제다.

해결하자.

@Transactional
public void clearFileFromProduct(Product product) {
List<PhotoFile> photoFile = product.getPhotoFile();

    if (photoFile.size() > 0) {
         photoFile.forEach(f -> s3UploaderService.deleteS3(f.getHashFilename()));
         
         **product.deletePhotoFile(); ** // 추가
         
         fileRepository.deleteRelatedProductId(product.getId());
    }
}

위와 같이 Product가 참조하고 있는 PhotoFile을 제거하면, 영속성 전이를 수행할 엔티티 참조 관계가 더이상 존재하지 않으므로 정상적으로 PhotoFile 제거 -> Product 제거를 수행할 수 있다.

결론

  • 양방향 연관관계는 추가할 때도, 삭제 할 때도 늘 함께 움직이는 것이 좋다.
profile
스프링 깎는 노인

0개의 댓글