[volume-4] 쿠폰 도메인 설계 및 동시성 제어 #159
Merged
plan11plan merged 172 commits intoLoopers-dev-lab:plan11planfrom Mar 12, 2026
Merged
Conversation
- Password VO를 EncryptedPassword로 변경하여 암호화된 비밀번호임을 명확히 표현 - 서비스 계층에 SignupCommand, ChangePasswordCommand, UserInfo DTO 도입 - 컨트롤러에서 엔티티(UserModel) 직접 노출 제거 - Example 관련 코드 삭제 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- EncryptedPassword.of() 오버로드 제거 → 하나만 유지 (형식 검증 + 암호화) - UserModel 생성자에서 rawPassword + encoder를 받아 birthDate 교차 검증 수행 - 생성/변경 두 경로 모두 UserModel이 검증 → 일관성 확보 - 패스워드 설계 결정 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…FLICT 예외로 변환 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 205 files, which is 55 over the limit of 150. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (50)
📒 Files selected for processing (205)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📌 Summary
쿠폰 도메인 설계/구현 완료. 낙관락을 기본 전략으로 채택했으나,
주문 흐름처럼 트랜잭션이 긴 유스케이스에서의 재시도 비용 문제를 발견했고,
이 판단이 맞는 방향인지 멘토님 피드백을 구하고 싶습니다.
과제 요약
리뷰 포인트 및 질문
1. 락 선택기준 - 낙관락을 기본 전략으로 선택한 이유
-충돌과 상관없이, 낙관락이 비관락보다 DB 커넥션 점유 시간과 자원 효율 면에서 유리하다고 판단했습니다.
2. 주문같이 긴 트랜잭션에서의 락 선택과 개선법 질문
현재 주문 생성 흐름은 아래와 같습니다.

저의 현재 판단
질문
고민하다 보니 더 혼란이 생겼습니다..
"긴 트랜잭션에서의 동시성 문제를 락 전략(낙관 → 비관 전환)으로 해결하려는 게 맞는 접근인가,
아니면 트랜잭션을 분리하고 실패 시 보상하는 구조처럼 다른 차원에서 접근해야 하는 문제인가?"
Claude는 트랜잭션 분리 + 보상 이벤트를 제안했습니다.
저는 아래가 아직 정리되지 않았고, 이에 대한 멘토님의 조언 및 의견을 구하고, 학습 및 개선으로 이루고 싶습니다.
최종적으로 멘토님께 얻고 싶은 것
round4 과제 진행 내용
동시성 이슈 발생 지점 리스트업 및 테스트 진행
프로젝트에서 동시성 이슈가 발생할 수 있는 지점을 모두 식별하고, 각각 어떻게 해결했는지 정리했다.
1. 쿠폰 동시 발급 —
CouponModel@Version+@Retryable(50회))issuedQuantity++시 버전 충돌 감지, 충돌 시 50ms + random 후 재시도2. 쿠폰 동시 사용 —
OwnedCouponModelUPDATE ... SET order_id = ? WHERE id = ? AND order_id IS NULL)3. 재고 동시 차감 —
ProductModel@Version), OrderFacade의@Retryable(10회)로 커버4. 포인트 동시 차감 —
UserModel@Version), OrderFacade의@Retryable(10회)로 커버5. 주문 동시 생성 —
OrderModel@Version+@Retryable(10회))6. 주문 아이템 동시 취소 —
OrderModel@Version+@Retryable(5회))7. 좋아요 동시 등록 —
ProductLikeModelDataIntegrityViolationException→ CONFLICT)@Version없이 DB UniqueConstraint로 원천 차단결과: 동시성 테스트 검증 완료
각 시나리오별 동시성 테스트를 작성하여 데이터 정합성을 검증했다.
CouponIssueConcurrencyTestissuedQuantity == 실제 발급 수OwnedCouponUseConcurrencyTestStockDeductionConcurrencyTestPointDeductionConcurrencyTestOrderItemCancelConcurrencyTestProductLikeConcurrencyTest🏗️ 쿠폰 Design Overview
주요 컴포넌트 책임
CouponModel: 쿠폰 템플릿. 발급 가능 여부 검증(validateIssuable), 수량 증가(issue).@Version으로 동시 발급 제어OwnedCouponModel: 발급된 쿠폰 인스턴스. Coupon 스냅샷 보유. 사용/복원/할인 계산. 상태는orderId+expiredAt에서 파생 (AVAILABLE/USED/EXPIRED)CouponService: 도메인 서비스. 쿠폰 등록/수정/삭제/발급, CAS Update 기반 사용, 주문 취소 시 복원CouponFacade: 트랜잭션 경계 +@Retryable재시도. 낙관적 락 충돌 시 최대 50회 재시도OrderFacade: 쿠폰 적용 주문 생성 + 취소 시 쿠폰 복원 조율.@Retryable최대 10회 재시도🔁 Flow Diagram
쿠폰 발급 (낙관적 락 + 재시도)
sequenceDiagram autonumber participant Client participant Controller participant CouponFacade participant CouponService participant DB Client->>Controller: POST /api/v1/coupons/{id}/issue Controller->>CouponFacade: issueCoupon(couponId, userId) Note over CouponFacade: @Retryable(maxAttempts=50) CouponFacade->>CouponService: issue(couponId, userId) CouponService->>DB: SELECT coupon (version=N) DB-->>CouponService: CouponModel Note over CouponService: validateIssuable()<br/>중복 발급 체크 CouponService->>DB: UPDATE coupon SET version=N+1 alt 버전 충돌 DB-->>CouponService: OptimisticLockException Note over CouponFacade: 50ms + random 후 재시도 else 성공 CouponService->>DB: INSERT owned_coupon (스냅샷) DB-->>CouponService: OK end CouponFacade-->>Controller: 발급 결과 Controller-->>Client: 201 Created쿠폰 적용 주문 생성 (CAS Update)
sequenceDiagram autonumber participant Client participant OrderFacade participant ProductService participant OrderService participant CouponService participant DB Client->>OrderFacade: 주문 요청 (items, couponId) OrderFacade->>ProductService: 재고 검증 + 차감 OrderFacade->>OrderService: 주문 생성 (discountAmount=0) OrderService-->>OrderFacade: Order (orderId) alt couponId != null OrderFacade->>CouponService: useAndCalculateDiscount() CouponService->>DB: UPDATE owned_coupon<br/>SET order_id=? WHERE id=? AND order_id IS NULL alt affected rows = 0 Note over CouponService: 이미 사용된 쿠폰 → 예외<br/>트랜잭션 롤백 else affected rows = 1 CouponService-->>OrderFacade: discountAmount Note over OrderFacade: order.applyDiscount() end end OrderFacade-->>Client: 주문 완료📦 쿠폰 도메인 모델링
설계 핵심 결정
orderId/expiredAt으로 AVAILABLE/USED/EXPIRED 동적 판정. 상태 불일치 원천 차단WHERE order_id IS NULL단일 UPDATE의 원자성 활용. 별도 락 없이 동시 사용 방지ERD
erDiagram coupons { bigint id PK varchar name varchar discount_type "FIXED / RATE" bigint discount_value bigint min_order_amount "nullable" int total_quantity int issued_quantity bigint version "낙관적 락" timestamp expired_at timestamp created_at timestamp updated_at timestamp deleted_at "Soft Delete" } owned_coupons { bigint id PK bigint coupon_id "ID 참조 (FK 없음)" varchar coupon_name "스냅샷" varchar discount_type "스냅샷" bigint discount_value "스냅샷" bigint min_order_amount "스냅샷" timestamp expired_at "스냅샷" bigint user_id bigint order_id "nullable — CAS Update 대상" timestamp used_at "nullable" timestamp created_at timestamp updated_at timestamp deleted_at } coupons ||..o{ owned_coupons : "couponId" users ||--o{ owned_coupons : "userId" owned_coupons |o--o| orders : "orderId (nullable)"인덱스
uk_owned_coupon_user(coupon_id, user_id)UNIQUEidx_owned_coupon_user(user_id)idx_owned_coupon_coupon(coupon_id)API 목록
/api/v1/coupons/{couponId}/issue/api/v1/users/me/coupons/api-admin/v1/coupons/api-admin/v1/coupons/{couponId}/api-admin/v1/coupons/{couponId}/api-admin/v1/coupons/api-admin/v1/coupons/{couponId}/api-admin/v1/coupons/{couponId}/issues✅ Checklist
AI로 체크리스트 누수 점검을 진행했습니다.
🗞️ Coupon 도메인
🧾 주문
🧪 동시성 테스트