-
동시성문제 - 비관적 락이커머스 devops 2026. 1. 3. 15:53
[ Thread A와 Thread B가 동시에 실행 ] Thread A Thread B ───────────────────────────────────────────────────── 1. status 확인 (AVAILABLE) 1. status 확인 (AVAILABLE) 2. user 확인 (null) 2. user 확인 (null) 3. assignToUser(A) 실행 중... 3. assignToUser(B) 실행! 4. 커밋 완료 (B가 쿠폰 획득) 4. 커밋 실패 또는 덮어쓰기문제 상황 : 스레드a, 스레드b가 동시 실행했을 때
@Repository public interface CouponUserRepository extends JpaRepository<CouponUser, Long> { /** * 쿠폰 코드로 조회 (비관적 락) */ @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT cu FROM CouponUser cu JOIN FETCH cu.coupon WHERE cu.code = :code") Optional<CouponUser> findByCodeWithCouponForUpdate(@Param("code") String code); }- 비관적 락으로 조회 추가 : SELECT 시점부터 락을 걸어 다른 트랜잭션이 같은 데이터를 읽지 못하게 동작
/** * 쿠폰 등록 */ public RegisterCouponResponse registerCoupon (String couponCode, Account account) { // 1. 쿠폰 코드로 조회 CouponUser couponUser = couponUserRepository.findByCodeWithCoupon(couponCode) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.INVALID_COUPON_CODE)); Long userId = account.getAccountId(); // 2. 유효성 검증 validateCouponRegistration(couponUser, userId); // 3. 사용자 조회 User user = userRepository.findById(userId) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.CANNOT_FOUND_USER)); // 4. 쿠폰 등록 (AVAILABLE → ISSUED) couponUser.assignToUser(user); return couponMapper.toRegisterCouponResponse(couponUser); }@Transactional public RegisterCouponResponse registerCoupon(String couponCode, Account account) { // 비관적 락으로 조회 (다른 트랜잭션이 접근 불가) CouponUser couponUser = couponUserRepository.findByCodeWithCouponForUpdate(couponCode) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.INVALID_COUPON_CODE)); Long userId = account.getAccountId(); // 2. 유효성 검증 validateCouponRegistration(couponUser, userId); // 3. 사용자 조회 User user = userRepository.findById(userId) .orElseThrow(() -> new ServiceException(ServiceExceptionCode.CANNOT_FOUND_USER)); // 4. 쿠폰 등록 couponUser.assignToUser(user); return couponMapper.toRegisterCouponResponse(couponUser); }- 쿠폰 등록 로직 수정
[ Thread A와 Thread B가 동시에 실행 ] Thread A Thread B ───────────────────────────────────────────────────────────────── [트랜잭션 A 시작] [트랜잭션 B 시작] 1. SELECT * FROM coupon_users 1. SELECT * FROM coupon_users WHERE code = 'XXX' WHERE code = 'XXX' FOR UPDATE; 🔒 FOR UPDATE; 🔒 → DB에 배타적 락 획득! → 대기... (락 획득 대기) → status: AVAILABLE → user_id: null 2. 검증 통과 [대기 중...] 3. UPDATE coupon_users [대기 중...] SET user_id = A, status = ISSUED WHERE id = 1 실행 완료 4. [트랜잭션 A 커밋] [대기 중...] 🔓 락 해제 → 락 획득 🔒 2. SELECT 결과 받음 → status: ISSUED → user_id: A 3. 검증 실패 : throw ALREADY_REGISTERED_COUPON 4. [트랜잭션 B 롤백] 결과: A만 쿠폰 획득 (선착순 보장)728x90'이커머스 devops' 카테고리의 다른 글
현재 Spring Security 구조 (0) 2026.01.07 로그인/로그아웃 인증 개선 (0) 2026.01.07 스프링부트게시판 (4) (0) 2025.11.29 스프링부트 게시판 (3) (0) 2025.11.28 스프링부트 게시판 (2) (0) 2025.11.27