쿠폰 발급에서 쿠폰 재고 수량은 공유 자원이기 때문에 여러 사용자가 동시에 쿠폰 발급을 요청하면 동시성 문제가 발생합니다.
여러 트랜잭션이 동일한 재고 값을 읽은 후 업데이트를 수행하면 쿠폰이 과발급 될 수 있습니다.
현재 시나리오 테스트에서는 쿠폰 수량을 100개로 생성하고, 200명이 동시에 쿠폰 발급을 요청하는 상황을 가정합니다.

@Test
@DisplayName("쿠폰 동시성 테스트")
void testConcurrentCouponIssuance() throws Exception {
// 동시에 쿠폰 발급 요청
int threadCount = 200;
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(threadCount);
// 총 수행 시간 측정
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
final long userId = i; // 각 쓰레드마다 고유한 사용자 ID 사용
executorService.execute(() -> {
try {
CouponIssueRequestDto couponIssueRequestDto = CouponIssueRequestDto.builder()
.promotionId(promotionId)
.couponId(couponId)
.userId(userId)
.build();
// 쿠폰 발급
couponApplicationService.issueCoupon(couponIssueRequestDto);
} catch (Exception e) {
// 예상되는 예외 처리 (쿠폰 소진 예외 등)
System.out.println("Exception: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
executorService.shutdown();
long end = System.currentTimeMillis();
// 총 수행 시간 출력
System.out.println("총 수행 시간: " + (end - start) + "ms");
int issuedCouponCount = couponUserRepository.countByCouponId(couponId);
System.out.println("발급된 쿠폰 수량: " + issuedCouponCount);
// 발급된 쿠폰 수량이 100개인지 확인
Assertions.assertThat(issuedCouponCount).isEqualTo(100);
}
}
100개의 사용자 쿠폰을 발급하고 쿠폰 수량이 0이되면 예외가 발생해야하지만, 예상과 다르게 요청에 따라 사용자 쿠폰이 200개가 모두 발급 되고, 쿠폰 수량은 72개로 제대로 차감이 되지 않은 결과입니다.
public void issueCoupon() {
if (this.totalQuantity <= 0) {
throw new IllegalArgumentException("쿠폰 발급 가능 수량이 없습니다.");
}
this.totalQuantity--;
}


this.totalQuantity--는 현재 스레드 간 안전하지 않은 연산입니다. coupon.issueCoupon() 메서드를 호출할 때, 각 스레드가 쿠폰의 totalQuantity 필드를 동시에 읽고 쓰는 과정에서 **경쟁 조건(Race Condition)**이 발생하여, 쿠폰 재고가 예상한 대로 감소하지 않게 됩니다.
이로 인해 200개의 요청이 모두 성공적으로 처리되고, 쿠폰 수량이 비정상적으로 남게 됩니다.
이러한 동시성 문제를 해결하기 위해서는 동시에 여러 스레드가 해당 자원을 접근할 수 없도록 해야합니다.
UPDATE (PostgreSQL 기준)PostgreSQL에서는 값을 업데이트 할 때 X-Lock(Exclusive Lock)을 이용하여 데이터베이스 차원에서 해당 행(row)에 대해 배타적 락이 걸리며, 다른 트랜잭션이 해당 행을 수정하거나 삭제하는 것을 방지합니다.
따라서 데이터베이스 상에서 동시성 처리를 위해 @Modifying로 UPDATE 쿼리를 이용하여 쿠폰 수량을 차감 처리합니다.
@Modifying
@Query("UPDATE Coupon c SET c.totalQuantity = c.totalQuantity - 1 WHERE c.couponId = :couponId AND c.totalQuantity > 0")
int decreaseCouponQuantity(@Param("couponId") UUID couponId);