@Transactional
이 없는 상태에서 트랜잭션이 제대로 관리되지 않아 락이 즉시 해제되었음.@Transactional
추가CouponService
의 issueCoupon
메서드에 트랜잭션을 적용.@Transactional
public void issueCoupon(Long couponId, RequestDto request, Member member) {
Coupon coupon = couponRepository.findByIdWithPessimisticLock(couponId)
.orElseThrow(() -> new GlobalException(COUPON_NOT_FOUND));
if (issuanceRepository.findByMemberIdAndCouponId(member.getId(), couponId).isPresent()) {
throw new GlobalException(COUPON_ALREADY_ISSUED);
}
coupon.validateQuantity();
coupon.decreaseQuantity();
Issuance issuance = Issuance.builder()
.member(member)
.coupon(coupon)
.build();
issuanceRepository.save(issuance);
}
CouponRepository
에 비관적 락을 적용한 JPQL 작성:@Query("SELECT c FROM Coupon c WHERE c.id = :id FOR UPDATE")
Optional<Coupon> findByIdWithPessimisticLock(@Param("id") Long id);
Member
를 생성하여 동시성 문제를 재현.Thread 시작: 0
Thread 시작: 1
Thread 시작: 2
...
Thread 완료: 98
Thread 완료: 99
남은 쿠폰 수량: 0
발급된 쿠폰 수량: 100
@Transactional
은 트랜잭션의 경계를 명확히 설정하여 락의 생명 주기를 보장.비관적 락과 성능 분석:
테스트 시나리오 확장:
에러 로그 및 예외 처리 개선:
GlobalException
외에 동시성 제어 실패 로그를 추가하여 성능 병목점 분석.@Test
@DisplayName("비관적 락 동시성 제어 테스트")
void pessimisticLockConcurrencyTest() throws InterruptedException {
for (int i = 0; i < threadCount; i++) {
int threadId = i;
executorService.submit(() -> {
try {
Member threadMember = new Member("Test User " + threadId, "test" + threadId + "@example.com", "password", MemberRole.USER);
memberRepository.save(threadMember);
couponService.issueCoupon(coupon.getId(), new RequestDto(threadMember.getId()), threadMember);
} catch (GlobalException e) {
System.out.println("Thread " + threadId + " 예외 발생: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
Coupon updatedCoupon = couponRepository.findById(coupon.getId()).orElseThrow();
System.out.println("남은 쿠폰 수량: " + updatedCoupon.getQuantity());
System.out.println("발급된 쿠폰 수량: " + issuanceRepository.count());
assertEquals(0, updatedCoupon.getQuantity());
assertEquals(100, issuanceRepository.count());
}