비관적 락 적용기

문정현·2024년 1월 18일
0

우리 팀의 핵심이라고도 볼 수 있는 핫딜 기능은 순간 유저가 몰려서 그 동시성을 핸들해야 하는것이 핵심 과제였다.

        @Test
        @DisplayName("데이터 정합성 테스트 - 핫딜의 한정 수량이 정확히 감소하는지 확인")
        void purchaseHotdealWithPessimisticLock() throws InterruptedException {
            // given
            Product product = productRepository.save(TEST_PRODUCT);
            Hotdeal hotdeal = hotdealRepository.save(
                HotdealTestUtil.createHotdeal(TEST_START_DAY, TEST_DUE_DAY, TEST_DEAL_QUANTITY,
                    TEST_SALE, product));

            int beforeDealQuantity = hotdeal.getDealQuantity();

            // 3개 씩 구매
            int purchaseQuantity = 3;
            PurchaseHotdealRequestDto requestDto = HotdealTestUtil.createTestPurchaseHotdealRequestDto(
                hotdeal.getId(), purchaseQuantity);

			// 10명의 유저
            int numberOfThreads = 10;
            ExecutorService service = Executors.newFixedThreadPool(numberOfThreads);
            CountDownLatch latch = new CountDownLatch(numberOfThreads);

            // when
            for (int i = 0; i < numberOfThreads; i++) {
                service.execute(() -> {
                    try {
                        hotdealService.purchaseHotdeal(member, requestDto);
                    } finally {
                        latch.countDown();
                    }
                });
            }

            latch.await(60, TimeUnit.SECONDS);
            service.shutdown();

            // then
            Hotdeal foundHotdeal = hotdealRepository.findById(hotdeal.getId())
                .orElseThrow(() -> new AssertionError("핫딜을 찾을 수 없음"));
            assertEquals(beforeDealQuantity - numberOfThreads * purchaseQuantity,
                foundHotdeal.getDealQuantity()); // 100 - 10 * 3 = 70
        }

10명의 유저가 동시에 100개의 재고가 남아있는 핫딜을 구매했을때 막연하게 생각했을 때 30개를 구매 했으니 테스트를 해보았을때 남은 재고는 70이라고 예상을 할 것이다

하지만 당연하게도 테스트는 실패 이전에 포스트 했던 레이스 컨디션이 발생하는 것이다

https://velog.io/@foqlzm12345/%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BD-vs-%EB%B9%84%EA%B4%80%EC%A0%81

    @Override
    @Transactional
    public PurchaseHotdealResponseDto purchaseHotdeal(Member member, PurchaseHotdealRequestDto requestDto) {
        log.info("구매 시작");
        Hotdeal hotdeal = hotdealRepository.findByIdWithPessimisticWriteLock(requestDto.getHotdealId())
            .orElseThrow(() -> new GlobalException(HotdealErrorCode.NOT_FOUND_HOTDEAL));

        ...예외처리 생략

        if (hotdeal.getDealQuantity() < requestDto.getQuantity()) {
            throw new GlobalException(HotdealErrorCode.LACK_DEAL_QUANTITY); // 핫딜 구매시 재고보다 많은 수량 구매 시도
        }

		...중략

        purchase.getPurchaseProductList().add(purchaseProduct);
        purchase.updateTotalPrice(totalPrice);
        purchaseRepository.save(purchase);

        hotdeal.updateDealQuantity(hotdeal.getDealQuantity() - requestDto.getQuantity()); // 앞에서 예외처리 완료
        hotdealRepository.save(hotdeal); // 더티 체킹

        return HotdealMapper.INSTANCE.toPurchaseResponseDto(hotdeal);
    }

핫딜을 구매할 경우 Purchase의 총금액과 Hotdeal의 남은 수량을 update하고 있기 때문에 여기서 동시성 처리를 해주지 않는다면 데이터의 정합성을 보장할 수 없다.
findByIdWithPessimisticWriteLock 쿼리에 @Lock(LockModeType.PESSIMISTIC_WRITE) 어노테이션으로 비관적 락을 걸어줘야만 한다

profile
기록 == 성장

0개의 댓글