낙관적 락의 정합성이 틀어지는 경우

김파란·2024년 12월 12일

project

목록 보기
6/9

낙관적 락

  • 낙관적 락의 특성과 동장방식에 의해 정합성이 틀어질 수 있다
  • 낙관적 락은 충돌을 감지하지만, 충돌을 해결하지는 않기 때문에 특정 상황에서 데이터의 정합성이 깨질 수 있다

동작 원리

  • 데이터를 수정하기 전에 데이터가 다른 트랜잭션에서 수정되었는지 확인한다
  • Version을 통해 관리하며, 데이터가 수정될 때마다 버전 값이 증가한다
  • 커밋될 때 버전 값이 같으면 정상적으로 커밋되지만 다른 트랜잭셔네엇 변경된 경우 버전이 맞지 않아 충돌을 감지한다

정합성이 틀어지는 이유

(1). 이유 1

  • A와 B가 동시에 같은 데이터를 읽는다
  • A는 읽은 데이터를 바탕으로 연산을 수행하고 값을 업데이트한다 (이 시점에서 버전이 증가)
  • B도 읽은 데이터를 바탕으로 연산을 수행합니다. 그러나 B는 이전 버전의 데이터를 기준으로 연산하므로 최신 데이터를 반영하지 못한 상태에서 업데이트가 발생한다
  • 최종적으로 B의 업데이트가 성공하면 A의 수정 사항이 덮어씌워져 데이터 정합성이 깨진다
  • 트랜잭션이 완료되는 시점에 충돌을 감지하기 때문에 병렬 트랜잭션 실행 시에는 정합성이 깨지게 된다

(2). 이유 2

  • 낙관적 락은 단일 엔티티의 버전 번호 충돌만 감지한다. 하지만 여러 엔티티를 조회하거나 연관 데이터를 수정하는 로직에서는 충돌을 감지하지 못할 수 있습니다.

예시

  • 트랜잭션 A가 리뷰 데이터를 모두 조회하여 상품 점수를 계산한다
  • 트랜잭션 B가 별도의 리뷰 데이터를 추가하거나 삭제한다
  • A는 B의 변경 사항을 고려하지 않은 상태로 상품 점수를 업데이트하므로, 결과 데이터의 정합성이 틀어질 수 있다.

즉 전체 엔티티를 조회해서 점수를 계산하려고 하는데, 다른 트랜잭션이 추가 및 삭제가 가능해 정합성이 깨질 수 있다

나의 경우

  • Review를 저장하고 상품이 조회해서 점수를 계산을 하는 게 나의 목표이다
  • 리뷰를 조회하는 와중에 저장이 가능해져 점수 계산이 틀어지게 된다
public class ReviewService {

    private final ReviewRepository reviewRepository;
    private final ReviewRequestMapper reviewRequestMapper;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public void create(Long productId, ReviewCreateReqDto createReqDto, MultipartFile file) {
        Review review = reviewRequestMapper.create(productId, createReqDto);
        reviewRepository.save(review);

        eventPublisher.publishEvent(new ProductIncreaseEvent(productId, createReqDto.score()));
        eventPublisher.publishEvent(new ReviewImageFileEvent(file, review));
    }

낙관적 락

  • 저장은 계속 되고, 조회를 하고 수정을 해야하니 데이터 정합성이 깨지게 된다
  • 물론 데이터가 맞을 때까지 반복하면 되겠지만 비효율적이다
  • 이런 방법을 쓰지 말고, 조회와 리뷰 수를 저장하는 테이블을 하나 더 만들어 거기서 업데이트를 하는 방법이 좋아보인다
@Service
@RequiredArgsConstructor
public class OptimisticService {

    private final ReviewRepository reviewRepository;
    private final ProductRepository productRepository;

    // 낙관적 락
    @Transactional
    public void updateOptimistic(Long productId) {
        Product product = getProduct(productId);
        ReviewUpdate totalReviewScore = getTotalReviewScore(productId);
        product.updateReviewData(totalReviewScore.count(), totalReviewScore.totalScore());
        productRepository.modifyProductReviewStats(product.getReviewCount(), product.getScore(), product.getId());
    }

    private Product getProduct(Long productId) {
        return productRepository.findById(productId).orElseThrow(
                () -> new CustomApiException(ErrorMessage.NOT_FOUND_PRODUCT.getMessage())
        );
    }

    private ReviewUpdate getTotalReviewScore(Long productId) {
        List<Review> reviews = reviewRepository.findAllByOptimistic(productId);
        long count = reviews.size();
        float totalScore = (float) reviews.stream()
                .mapToDouble(Review::getScore)
                .sum();
        return new ReviewUpdate(count, totalScore);
    }

    record ReviewUpdate(long count, float totalScore) {
    }
}

0개의 댓글