Files
sodalive-backend-spring-boot/docs/plan-task/20260518_쿠폰충전회원락보강.md

3.6 KiB

쿠폰 충전 회원 락 보강 작업 계획

목적

  • 쿠폰 캔 지급 흐름에서도 일반 결제 완료 흐름과 동일하게 회원 row lock을 잡은 뒤 회원 잔액을 증가시킨다.
  • 동시 충전/쿠폰 사용 상황에서 Member.charge(...)+= 기반 잔액 증가가 유실되지 않도록 한다.

현재 확인된 문제

  • ChargeService.chargeByCoupon(...)CouponType.CAN 분기에서 컨트롤러/인증 계층이 전달한 member 인스턴스에 바로 member.charge(0, coupon.can, "pg")를 호출한다.
  • 일반 결제 완료 경로(payverseWebhook, payverseVerify, verify, verifyHecto, appleVerify, processGoogleIap)는 memberRepository.findByIdForUpdate(...)로 회원 row를 잠근 뒤 member.charge(...)를 호출한다.
  • 쿠폰 충전만 이 패턴과 달라, 같은 회원에게 쿠폰 충전과 다른 잔액 증가 작업이 동시에 실행될 경우 회원 캔 잔액 유실 가능성이 남는다.

확정 범위

  • ChargeService.chargeByCoupon(...)CouponType.CAN 분기만 최소 수정한다.
  • 쿠폰 번호 검증, 이미 사용된 쿠폰 검증, Charge(status = COUPON) 생성, 성공 메시지 반환 동작은 유지한다.
  • 포인트 쿠폰(CouponType.POINT) 로직은 변경하지 않는다.
  • 공개 API 요청/응답 스키마는 변경하지 않는다.
  • 본 문서를 기준으로 최소 구현을 진행한다.

구현 항목

  • 쿠폰 캔 지급 lock 패턴 적용

    • chargeByCoupon(...)에서 member.id!!를 기준으로 memberRepository.findByIdForUpdate(...)를 호출한다.
    • lock 조회로 얻은 회원 엔티티를 couponCharge.member에 연결한다.
    • lock 조회로 얻은 회원 엔티티에 member.charge(0, coupon.can, "pg")를 호출한다.
    • lock 조회 실패 시 기존 인증 실패 계열 예외 메시지 패턴을 따른다.
  • 회귀 테스트 추가

    • CouponType.CAN 쿠폰 사용 시 memberRepository.findByIdForUpdate(...)가 호출되는지 검증한다.
    • lock 조회로 얻은 회원에 쿠폰 캔이 반영되는지 검증한다.
    • 이미 사용된 쿠폰이면 lock 조회와 캔 지급이 진행되지 않는지 검증한다.
    • CouponType.POINT 쿠폰 동작이 변경되지 않았는지 기존 또는 신규 테스트로 확인한다.
  • 최소 구현 검증

    • 관련 테스트를 먼저 실행해 실패를 확인한다.
    • 구현 후 관련 테스트가 통과하는지 확인한다.
    • ./gradlew ktlintCheck를 실행한다.
    • 필요 시 ./gradlew test로 전체 회귀를 확인한다.

검증 항목

  • 쿠폰 캔 지급 테스트 통과
  • 쿠폰 포인트 지급 기존 동작 유지 확인
  • ./gradlew ktlintCheck
  • ./gradlew test

검증 로그

  • 문서 작성 검증: ChargeService.chargeByCoupon(...)CouponType.CAN 분기와 일반 결제 완료 경로의 memberRepository.findByIdForUpdate(...) 사용 패턴을 확인하고 작업 범위를 문서화했다.
  • RED 검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.can.charge.ChargeServiceTest' 실행 시 WantedButNotInvokedmemberRepository.findByIdForUpdate(10L) 미호출 실패를 확인했다.
  • 구현 검증: CouponType.CAN 분기에서 locked member를 조회하고, couponCharge.membermember.charge(...) 모두 locked member를 사용하도록 변경했다.
  • 관련 테스트 검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.can.charge.ChargeServiceTest' 통과를 확인했다.
  • 린트 검증: ./gradlew ktlintCheck 통과를 확인했다.
  • 전체 테스트 검증: ./gradlew test 통과를 확인했다.