[volume-4] 쿠폰 도메인 설계 및 동시성 제어#161
Conversation
- CLAUDE.md 추가 (프로젝트 컨텍스트 및 개발 규칙) - spring-security-crypto 의존성 추가 - ErrorType에 UNAUTHORIZED, USER_NOT_FOUND, PASSWORD_MISMATCH 추가 - MySqlTestContainersConfig에 MYSQL_ROOT_PASSWORD 환경변수 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- User 엔티티 (필드 검증, BCrypt 암호화, 이름 마스킹) - UserRepository 인터페이스 - UserService (회원가입, 조회, 인증, 비밀번호 변경) - UserTest 단위 테스트 47건 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UserJpaRepository (Spring Data JPA) - UserRepositoryImpl (Repository 구현체) - UserServiceIntegrationTest 통합 테스트 9건 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UserFacade, UserInfo (Application 계층) - AuthenticatedUser, AuthenticatedUserArgumentResolver (헤더 인증) - WebMvcConfig (ArgumentResolver 등록) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UserV1Controller (POST /users, GET /users/me, PATCH /users/me/password) - UserV1Dto (요청/응답 DTO) - UserV1ApiSpec (OpenAPI 스펙) - UserV1ApiE2ETest E2E 테스트 12건 - user-v1.http (IntelliJ HTTP Client) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- .claude/commands/create-pr.md (PR 템플릿 기반 자동 생성 스킬) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 01-requirements.md: 도메인별 필드/비즈니스 규칙, 유저 시나리오 - 02-sequence-diagrams.md: 주문/좋아요/브랜드 삭제 시퀀스 다이어그램 - 03-class-diagram.md: 계층별 클래스 구조 다이어그램 - 04-erd.md: 테이블 스키마, 인덱스, FK 정책 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: User 도메인 구현 (회원가입, 내 정보 조회, 비밀번호 변경)
This reverts commit 44bfc36.
- Brand/Product/ProductLike/Order/OrderItem 도메인 필드 정의 - 비즈니스 규칙 (BR-*) 및 검증 규칙 정의 - 유저 시나리오 9개 (US-001~009), 어드민 시나리오 7개 (AS-001~007) - API 명세 및 에러 타입 정의 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 주문 생성 시퀀스 (정상/재고 부족 플로우) - 좋아요 등록 시퀀스 (토글 방식: 신규/취소) - 브랜드 삭제 시퀀스 (Cascade 삭제) - 상품 목록 조회 시퀀스 (좋아요 수 포함) - 어드민 인증 플로우 (Interceptor + ArgumentResolver) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 전체 계층 구조 개요 (Layered Architecture) - Brand/Product/ProductLike/Order 도메인 클래스 - 인증 관련 클래스 (AdminAuthInterceptor, AdminUserArgumentResolver) - 공통 클래스 (BaseEntity, ApiResponse, CoreException, ErrorType) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 6개 테이블 스키마 (users, brands, products, product_likes, orders, order_items) - 인덱스 설계 및 FK 삭제 정책 - 쿼리 최적화 가이드 (좋아요순 정렬, 비관적 락) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- requirements-analysis 스킬 정의 - 요구사항 분석 워크플로우 가이드라인 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Money: 금액 VO, 0 이상 검증, add/multiply 연산 - Stock: 재고 VO, 차감/증가 시 불변식 검증 - Quantity: 수량 VO, 1 이상 검증 - ProductSort: 상품 정렬 Enum (LATEST, PRICE_ASC, LIKES_DESC) - 각 VO에 대한 단위 테스트 포함 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java): - Brand: 브랜드 엔티티, create/reconstitute 정적 팩토리 - BrandRepository: Repository 인터페이스 - BrandDomainService: 중복 이름 검증, CRUD 정책 - BrandValidator: 브랜드 존재 검증 Infrastructure Layer: - BrandJpaEntity: JPA 엔티티 (@entity) - BrandMapper: Domain ↔ JPA 변환 - BrandRepositoryImpl: Repository Adapter Test: - FakeBrandRepository: Map 기반 in-memory 구현 - BrandTest, BrandInfoTest: 도메인 단위 테스트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java): - Product: 상품 엔티티, Money/Stock VO 사용 - ProductRepository: Repository 인터페이스 (비관적 락 포함) - ProductDomainService: 재고 차감, CRUD 정책 - ProductValidator: 브랜드 존재 검증 Infrastructure Layer: - ProductJpaEntity: JPA 엔티티 - ProductMapper: Domain ↔ JPA 변환 - ProductRepositoryImpl: Repository Adapter Test: - FakeProductRepository: Map 기반 in-memory 구현 - ProductTest: 재고 차감, soft delete 테스트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java): - Like: 좋아요 엔티티 (userId + productId) - LikeId: 복합키 VO - LikeRepository: Repository 인터페이스 - LikeDomainService: 중복 좋아요 방지, 멱등 취소 정책 Infrastructure Layer: - LikeJpaEntity: JPA 엔티티 (unique constraint) - LikeMapper: Domain ↔ JPA 변환 - LikeRepositoryImpl: Repository Adapter Test: - FakeLikeRepository: Map 기반 in-memory 구현 - LikeTest, LikeDomainServiceTest: 도메인 단위 테스트 정책: - 중복 좋아요 시 CONFLICT 예외 - 좋아요 취소는 멱등 (없어도 예외 없음) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Domain Layer (순수 Java): - Order: 주문 Aggregate Root, 총액 자동 계산 - OrderItem: 주문 항목, 가격 스냅샷 보관 - OrderStatus: 주문 상태 Enum - OrderRepository: Repository 인터페이스 Infrastructure Layer: - OrderJpaEntity: JPA 엔티티 (CascadeType.ALL) - OrderItemJpaEntity: JPA 엔티티 - OrderMapper: Domain ↔ JPA 변환 - OrderRepositoryImpl: Repository Adapter Test: - FakeOrderRepository: Map 기반 in-memory 구현 - OrderTest, OrderItemTest: 도메인 단위 테스트 불변식: - 주문 항목은 1개 이상 - 총액은 OrderItem 합산으로 계산 - OrderItem은 불변 리스트로 보호 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand: - BrandService: 브랜드 CRUD 유스케이스 - BrandResult: 응답 DTO Product: - ProductService: 상품 CRUD, 목록 조회 - ProductResult: 응답 DTO Like: - LikeApplicationService: 상품 검증 + 좋아요/취소 - LikeResult: 응답 DTO Order: - OrderApplicationService: 재고 차감 + 주문 생성 - OrderResult, OrderItemResult: 응답 DTO - OrderItemRequest: 요청 DTO Test: - BrandServiceIntegrationTest: 통합 테스트 - LikeApplicationServiceTest: Fake 기반 단위 테스트 - OrderApplicationServiceTest: Fake 기반 단위 테스트 트랜잭션: - 주문 시 비관적 락으로 재고 차감 - @transactional 경계 관리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand API: - BrandV1Controller: 브랜드 목록/상세 조회 - BrandAdminV1Controller: 브랜드 CRUD (관리자) - BrandV1Dto: 요청/응답 DTO - BrandV1ApiSpec, BrandAdminV1ApiSpec: OpenAPI 문서화 Product API: - ProductV1Controller: 상품 목록/상세 조회 - ProductAdminV1Controller: 상품 CRUD (관리자) - ProductV1Dto: 요청/응답 DTO - ProductV1ApiSpec, ProductAdminV1ApiSpec: OpenAPI 문서화 HTTP 테스트 파일: - brand-api.http, brand-admin-api.http - product-api.http, product-admin-api.http Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- domain-implements-task.md: 순수 DDD 구현 계획서 - Domain Layer 순수 Java 원칙 - 애그리게잇 설계 (Order, Product, Brand, Like) - Value Object 설계 (Money, Stock, Quantity) - Repository Interface/Impl 분리 - Fake Repository 테스트 전략 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brand: - BRAND_NOT_FOUND: 브랜드 미존재 - BRAND_ALREADY_EXISTS: 브랜드명 중복 - BRAND_DELETED: 삭제된 브랜드 Product: - PRODUCT_NOT_FOUND: 상품 미존재 - PRODUCT_DELETED: 삭제된 상품 - INSUFFICIENT_STOCK: 재고 부족 Order: - ORDER_NOT_FOUND: 주문 미존재 - ORDER_ACCESS_DENIED: 주문 접근 권한 없음 Admin: - ADMIN_UNAUTHORIZED: 관리자 권한 필요 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CouponTemplate: 쿠폰 정의 Aggregate Root (정액/정률 타입) - IssuedCoupon: 발급된 쿠폰 엔티티 (사용/만료 상태 관리) - CouponDiscountPolicy: 할인 계산 전략 패턴 - UserPoint: 포인트 도메인 (충전/사용) - Money.subtract(): 할인 계산용 메서드 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CouponTemplate/IssuedCoupon JPA 엔티티 및 Repository - UserPoint JPA 엔티티 및 Repository - 비관적 락(PESSIMISTIC_WRITE) 적용: findByIdWithLock - Order 엔티티 할인 필드 확장 (originalAmount, couponDiscount, pointDiscount) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CouponAdminService: 관리자 쿠폰 CRUD - CouponUserService: 사용자 쿠폰 발급/조회 (비관적 락) - UserPointService: 포인트 조회/충전 - OrderApplicationService.placeOrderWithDiscount(): 할인 적용 주문 - Order.createWithDiscount(): 할인 정보 포함 주문 생성 - 도메인별 에러 타입 추가 (COUPON_*, POINT_*, ORDER_*) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CouponAdminV1Controller: /api-admin/v1/coupons/* - CouponV1Controller: /api/v1/coupons/*/issue, /api/v1/users/me/coupons - PointV1Controller: /api/v1/users/me/points - OrderV1Controller: /api/v1/orders/with-discount 추가 - @userid + UserIdArgumentResolver: X-User-Id 헤더 인증 처리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- OrderConcurrencyTest: 재고/쿠폰/포인트 동시 차감 정합성 - CouponConcurrencyTest: 쿠폰 발급 한도 및 중복 발급 방지 - LikeConcurrencyTest: 좋아요 동시 요청 정합성 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- coupon-admin.http: 관리자 쿠폰 API - coupon-user.http: 사용자 쿠폰 API - point.http: 포인트 API - order-with-coupon.http: 할인 적용 주문 API Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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 |
|
@coderabbitai 아래 체크리스트가 맞게 동작되는지 확인해 🗞️ Coupon 도메인
🧾 주문
🧪 동시성 테스트
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 16085 🏁 Script executed: Length of output: 36999 🏁 Script executed: Length of output: 19288 🏁 Script executed: Length of output: 38246
🗞️ Coupon 도메인
🧾 주문
🧪 동시성 테스트
|
📌 Summary
🧭 Context & Decision
문제 정의
선택지와 결정
1. 트랜잭션 경계
A: 각 서비스별 개별 트랜잭션 처리
B: Application Layer(OrderFacade)에서 주문 전체를 하나의 트랜잭션으로 처리
2. 쿠폰 모델링 방식
A: 쿠폰 정책과 사용자 발급 쿠폰을 하나의 엔티티/테이블로 통합
B: CouponTemplate(정책)과 IssuedCoupon(사용자 발급 쿠폰)으로 분리
3. 쿠폰 할인 로직 구조
A: 주문 서비스 내부에서 if-else로 FIXED/RATE 분기 처리
B: 정책 객체(전략 패턴)로 할인 로직 분리
4. 쿠폰 사용 동시성 제어
A: 낙관적 락 (버전 체크 + 재시도)
B: 비관적 락 (SELECT FOR UPDATE / PESSIMISTIC_WRITE)
5. 재고 차감 동시성 제어
A: 낙관적 락 (@Version 기반 충돌 감지)
B: 비관적 락 (PESSIMISTIC_WRITE)
C: Atomic Update (stock = stock - quantity where stock >= quantity)
6. 좋아요 수 동시성 처리
A: 상품 엔티티 조회 후 likeCount 변경 후 저장
B: 비관적 락으로 보호
C: DB 원자적 증가/감소 쿼리 사용
7. 주문 금액 저장 방식
A: 주문에는 상품 ID만 저장하고 조회 시 현재 가격 기준으로 계산
B: 주문 시점 금액과 상품 정보를 스냅샷으로 저장
8. 만료 쿠폰 상태 처리
A: 배치/스케줄러로 EXPIRED 상태를 미리 갱신
B: 조회 시 expiredAt 기준으로 EXPIRED 계산
9. 실패 처리 방식
A: 가능한 작업만 반영하고 실패 항목만 별도 반환
B: 하나라도 실패하면 전체 롤백
10. 동시성 검증 방식
A: 단순 단위 테스트만 작성
B: 멀티스레드 기반 동시성 테스트까지 작성
11. 포인트 처리 방식
A: 포인트 차감을 주문 저장 이후 별도 처리
B: 주문 트랜잭션 안에서 함께 검증/차감
🏗️ Design Overview
변경 범위
domain/coupon/*: CouponTemplate, IssuedCoupon, CouponDiscountPolicydomain/point/*: UserPointinfrastructure/persistence/jpa/coupon/*,point/*application/coupon/*,point/*interfaces/api/coupon/*,point/*,order/*concurrency/*Test: 동시성 테스트주요 컴포넌트 책임
CouponTemplate: 쿠폰 정의 Aggregate Root (정액/정률 타입, 발급 한도 관리)IssuedCoupon: 발급된 쿠폰 엔티티 (사용/만료 상태 관리)CouponDiscountPolicy: 할인 계산 전략 패턴 (FixedCouponDiscountPolicy, RateCouponDiscountPolicy)UserPoint: 포인트 도메인 (충전/사용/잔액 검증)OrderApplicationService.placeOrderWithDiscount(): 할인 적용 주문 트랜잭션 관리UserIdArgumentResolver: X-User-Id 헤더 인증 처리🔁 Flow Diagram
할인 적용 주문 Flow (placeOrderWithDiscount)
sequenceDiagram autonumber participant Client participant OrderV1Controller participant OrderApplicationService participant IssuedCouponRepository participant ProductRepository participant UserPointRepository participant OrderRepository Client->>OrderV1Controller: POST /api/v1/orders/with-discount OrderV1Controller->>OrderApplicationService: placeOrderWithDiscount(userId, request) alt 쿠폰 적용 시 OrderApplicationService->>IssuedCouponRepository: findByIdWithLock(couponId) Note right of IssuedCouponRepository: PESSIMISTIC_WRITE IssuedCouponRepository-->>OrderApplicationService: IssuedCoupon OrderApplicationService->>OrderApplicationService: 쿠폰 검증 (소유자, 사용 가능 여부) end loop 각 주문 항목 OrderApplicationService->>ProductRepository: findByIdWithLock(productId) Note right of ProductRepository: PESSIMISTIC_WRITE ProductRepository-->>OrderApplicationService: Product OrderApplicationService->>OrderApplicationService: 재고 차감 end alt 포인트 적용 시 OrderApplicationService->>UserPointRepository: findByUserIdWithLock(userId) Note right of UserPointRepository: PESSIMISTIC_WRITE UserPointRepository-->>OrderApplicationService: UserPoint OrderApplicationService->>OrderApplicationService: 포인트 차감 end OrderApplicationService->>OrderApplicationService: 쿠폰 사용 처리 OrderApplicationService->>OrderRepository: save(Order) OrderRepository-->>OrderApplicationService: Order OrderApplicationService-->>OrderV1Controller: OrderResult OrderV1Controller-->>Client: 200 OK (OrderResponseDto)쿠폰 발급 Flow (issue)
sequenceDiagram autonumber participant Client participant CouponV1Controller participant CouponUserService participant CouponTemplateRepository participant IssuedCouponRepository Client->>CouponV1Controller: POST /api/v1/coupons/{couponId}/issue CouponV1Controller->>CouponUserService: issue(userId, couponTemplateId) CouponUserService->>CouponTemplateRepository: findByIdWithLock(couponTemplateId) Note right of CouponTemplateRepository: PESSIMISTIC_WRITE CouponTemplateRepository-->>CouponUserService: CouponTemplate alt 발급 가능 CouponUserService->>IssuedCouponRepository: existsByUserIdAndCouponTemplateId() IssuedCouponRepository-->>CouponUserService: false (중복 없음) CouponUserService->>CouponUserService: template.incrementIssuedCount() CouponUserService->>IssuedCouponRepository: save(IssuedCoupon) IssuedCouponRepository-->>CouponUserService: IssuedCoupon CouponUserService-->>CouponV1Controller: IssuedCouponResult CouponV1Controller-->>Client: 200 OK else 발급 불가 (한도 초과/만료/중복) CouponUserService-->>CouponV1Controller: CoreException CouponV1Controller-->>Client: 400/409 Error end예외 처리 Flow
sequenceDiagram autonumber participant Client participant OrderApplicationService participant DB Client->>OrderApplicationService: placeOrderWithDiscount() OrderApplicationService->>DB: BEGIN TRANSACTION alt 재고 부족 OrderApplicationService->>DB: SELECT product FOR UPDATE OrderApplicationService-->>Client: INSUFFICIENT_STOCK (400) OrderApplicationService->>DB: ROLLBACK else 쿠폰 사용 불가 OrderApplicationService->>DB: SELECT coupon FOR UPDATE OrderApplicationService-->>Client: COUPON_NOT_AVAILABLE (400) OrderApplicationService->>DB: ROLLBACK else 포인트 부족 OrderApplicationService->>DB: SELECT point FOR UPDATE OrderApplicationService-->>Client: INSUFFICIENT_POINT (400) OrderApplicationService->>DB: ROLLBACK else 성공 OrderApplicationService->>DB: UPDATE & INSERT OrderApplicationService->>DB: COMMIT OrderApplicationService-->>Client: 200 OK end🤖 Generated with Claude Code