우리 팀의 핵심이라고도 볼 수 있는 핫딜 기능은 순간 유저가 몰려서 그 동시성을 핸들해야 하는것이 핵심 과제였다.
@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) 어노테이션으로 비관적 락을 걸어줘야만 한다