Skip to content

[volume-4] 쿠폰 도메인 설계 및 동시성 제어#161

Merged
leeedohyun merged 30 commits intoLoopers-dev-lab:dame2from
dame2:volumn-4
Mar 12, 2026
Merged

[volume-4] 쿠폰 도메인 설계 및 동시성 제어#161
leeedohyun merged 30 commits intoLoopers-dev-lab:dame2from
dame2:volumn-4

Conversation

@dame2
Copy link

@dame2 dame2 commented Mar 6, 2026

📌 Summary

  • 배경: 주문 시 재고/쿠폰/포인트 차감이 개별적으로 처리되어 동시성 이슈(Lost Update) 발생 가능성 존재
  • 목표: 단일 트랜잭션 내에서 재고/쿠폰/포인트의 정합성을 보장하고, 비관적 락으로 동시성 이슈 방지
  • 결과: 쿠폰/포인트 도메인 신규 구현, 할인 적용 주문 API 추가, 동시성 테스트로 정합성 검증

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: 기존 주문 시스템은 재고만 관리하며, 쿠폰/포인트 할인 기능 미지원
  • 문제(또는 리스크):
    • 동시 주문 시 재고 초과 차감 가능
    • 같은 쿠폰 동시 사용 시 중복 사용 가능
    • 포인트 잔액 초과 사용 가능
  • 성공 기준:
    • 동시성 테스트에서 재고/쿠폰/포인트 정합성 100% 보장
    • 주문 실패 시 전체 롤백

선택지와 결정

1. 트랜잭션 경계

  • 고려한 대안:
    A: 각 서비스별 개별 트랜잭션 처리
    B: Application Layer(OrderFacade)에서 주문 전체를 하나의 트랜잭션으로 처리
  • 최종 결정: B: Application Layer(OrderFacade)에서 주문 전체를 하나의 트랜잭션으로 처리
  • 트레이드오프:
    • 장점: 주문/재고/쿠폰/포인트/주문 저장의 원자성 보장, 부분 성공 방지, 비즈니스 흐름이 명확함
    • 단점: 트랜잭션 범위가 넓어져 락 유지 시간이 길어질 수 있음
  • 추후 개선 여지: 주문 처리 단계가 더 복잡해지면 트랜잭션 내부 로직을 더 얇게 만들고, 비핵심 후처리는 이벤트 기반으로 분리 가능

2. 쿠폰 모델링 방식

  • 고려한 대안:
    A: 쿠폰 정책과 사용자 발급 쿠폰을 하나의 엔티티/테이블로 통합
    B: CouponTemplate(정책)과 IssuedCoupon(사용자 발급 쿠폰)으로 분리
  • 최종 결정: B: CouponTemplate과 IssuedCoupon을 분리
  • 트레이드오프:
    • 장점: 책임 분리가 명확함, 관리자 쿠폰 정의와 사용자 보유 쿠폰 상태를 자연스럽게 표현 가능, 확장성 높음
    • 단점: 테이블/조회 로직이 늘어나고 구현 복잡도가 약간 증가함
  • 추후 개선 여지: 쿠폰 발급 이력, 수량 제한, 선착순 쿠폰 같은 요구사항이 생겨도 구조 확장이 용이함

3. 쿠폰 할인 로직 구조

  • 고려한 대안:
    A: 주문 서비스 내부에서 if-else로 FIXED/RATE 분기 처리
    B: 정책 객체(전략 패턴)로 할인 로직 분리
  • 최종 결정: B: 정책 객체(전략 패턴)로 할인 로직 분리
  • 트레이드오프:
    • 장점: 할인 정책 확장에 유리함, 책임이 분리되어 테스트가 쉬움, 주문 서비스의 복잡도 감소
    • 단점: 초기 클래스 수가 늘어나고 단순 요구사항 대비 구조가 다소 커질 수 있음
  • 추후 개선 여지: 최대 할인 금액, 카테고리 전용 할인, 첫 주문 할인 등 신규 정책을 쉽게 추가 가능

4. 쿠폰 사용 동시성 제어

  • 고려한 대안:
    A: 낙관적 락 (버전 체크 + 재시도)
    B: 비관적 락 (SELECT FOR UPDATE / PESSIMISTIC_WRITE)
  • 최종 결정: B: 비관적 락 (PESSIMISTIC_WRITE)
  • 트레이드오프:
    • 장점: 강한 정합성 보장, 재시도 로직 불필요, 충돌 시 DB 레벨 대기, 동일 쿠폰 1회 사용 보장에 직관적
    • 단점: 높은 트래픽 시 락 경합 발생 가능
  • 추후 개선 여지: 트래픽이 커지면 상태 조건 기반 원자적 업데이트나 Redis 분산 락으로 확장 가능

5. 재고 차감 동시성 제어

  • 고려한 대안:
    A: 낙관적 락 (@Version 기반 충돌 감지)
    B: 비관적 락 (PESSIMISTIC_WRITE)
    C: Atomic Update (stock = stock - quantity where stock >= quantity)
  • 최종 결정: B: 비관적 락 (PESSIMISTIC_WRITE)
  • 트레이드오프:
    • 장점: 주문 전체 트랜잭션 안에서 재고 검증과 차감을 일관되게 처리 가능, 과제 설명이 직관적, overselling 방지에 명확함
    • 단점: 동시 주문이 많아질수록 락 대기와 경합 비용이 증가할 수 있음
  • 추후 개선 여지: 재고 차감만 고빈도 병목이 되면 Atomic Update 방식으로 전환하거나, 재고 전용 처리 전략으로 분리 가능

6. 좋아요 수 동시성 처리

  • 고려한 대안:
    A: 상품 엔티티 조회 후 likeCount 변경 후 저장
    B: 비관적 락으로 보호
    C: DB 원자적 증가/감소 쿼리 사용
  • 최종 결정: C: DB 원자적 증가/감소 쿼리 사용
  • 트레이드오프:
    • 장점: Lost Update 방지, 카운터성 데이터에 적합, 락 범위를 최소화할 수 있음, 구현 의도가 명확함
    • 단점: 도메인 메서드 중심 설계보다 쿼리 의존성이 커질 수 있음
  • 추후 개선 여지: 좋아요 엔티티와 중복 좋아요 방지 정책까지 강화되면 카운터와 사용자 액션 이력을 분리 관리할 수 있음

7. 주문 금액 저장 방식

  • 고려한 대안:
    A: 주문에는 상품 ID만 저장하고 조회 시 현재 가격 기준으로 계산
    B: 주문 시점 금액과 상품 정보를 스냅샷으로 저장
  • 최종 결정: B: 주문 시점 금액과 상품 정보를 스냅샷으로 저장
  • 트레이드오프:
    • 장점: 가격 변경 이후에도 주문 이력 보존 가능, 회계/정산/CS 대응에 유리, 주문 도메인 독립성 확보
    • 단점: 중복 데이터가 저장되어 저장 공간이 약간 증가함
  • 추후 개선 여지: 할인 상세 내역, 쿠폰 정책명, 세금/배송비까지 포함한 스냅샷으로 확장 가능

8. 만료 쿠폰 상태 처리

  • 고려한 대안:
    A: 배치/스케줄러로 EXPIRED 상태를 미리 갱신
    B: 조회 시 expiredAt 기준으로 EXPIRED 계산
  • 최종 결정: B: 조회 시 expiredAt 기준으로 EXPIRED 계산
  • 트레이드오프:
    • 장점: 구현이 단순함, 별도 배치 작업 불필요, 실시간 상태 반영 가능
    • 단점: 조회 시마다 상태 계산이 필요하고, 대량 통계 쿼리에는 비효율적일 수 있음
  • 추후 개선 여지: 쿠폰 수가 많아지고 운영성 요구가 커지면 배치 기반 상태 동기화로 전환 가능

9. 실패 처리 방식

  • 고려한 대안:
    A: 가능한 작업만 반영하고 실패 항목만 별도 반환
    B: 하나라도 실패하면 전체 롤백
  • 최종 결정: B: 하나라도 실패하면 전체 롤백
  • 트레이드오프:
    • 장점: 주문 도메인의 일관성 보장, 부분 반영으로 인한 데이터 불일치 방지, 사용자 기대와 비즈니스 규칙에 부합
    • 단점: 일부 성공 가능한 작업도 전체 실패 처리되므로 유연성은 낮음
  • 추후 개선 여지: 주문 외 후처리성 기능은 별도 이벤트/보상 트랜잭션으로 분리 가능

10. 동시성 검증 방식

  • 고려한 대안:
    A: 단순 단위 테스트만 작성
    B: 멀티스레드 기반 동시성 테스트까지 작성
  • 최종 결정: B: 멀티스레드 기반 동시성 테스트까지 작성
  • 트레이드오프:
    • 장점: 실제 경쟁 상황에서 정합성 검증 가능, 락/원자적 업데이트의 효과를 확인할 수 있음, 회귀 방지에 유리
    • 단점: 테스트가 무거워지고 환경에 따라 실행 시간이 늘어날 수 있음
  • 추후 개선 여지: 통합 테스트와 성능 테스트를 분리하고, 특정 케이스는 부하 테스트 도구로 확장 가능

11. 포인트 처리 방식

  • 고려한 대안:
    A: 포인트 차감을 주문 저장 이후 별도 처리
    B: 주문 트랜잭션 안에서 함께 검증/차감
  • 최종 결정: B: 주문 트랜잭션 안에서 함께 검증/차감
  • 트레이드오프:
    • 장점: 쿠폰/재고/포인트/주문 저장의 정합성을 함께 보장 가능, 부분 반영 방지
    • 단점: 주문 트랜잭션이 더 무거워질 수 있음
  • 추후 개선 여지: 포인트 적립은 주문 성공 후 비동기 이벤트로 분리하고, 차감만 동기 처리하는 방식으로 최적화 가능

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: order, coupon(신규), point(신규), product
  • 신규 추가:
    • domain/coupon/*: CouponTemplate, IssuedCoupon, CouponDiscountPolicy
    • domain/point/*: UserPoint
    • infrastructure/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)
Loading

쿠폰 발급 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
Loading

예외 처리 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
Loading

🤖 Generated with Claude Code

hanyoung-kurly and others added 30 commits February 2, 2026 01:26
- 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 도메인 구현 (회원가입, 내 정보 조회, 비밀번호 변경)
- 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>
@coderabbitai
Copy link

coderabbitai bot commented Mar 6, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 267b3bed-61e3-4930-99fd-cf3aeced7447

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@looper88888
Copy link
Contributor

@coderabbitai 아래 체크리스트가 맞게 동작되는지 확인해

🗞️ Coupon 도메인

  • 쿠폰은 사용자가 소유하고 있으며, 이미 사용된 쿠폰은 사용할 수 없어야 한다.
  • 쿠폰 종류는 정액 / 정률로 구분되며, 각 적용 로직을 구현하였다.
  • 각 발급된 쿠폰은 최대 한번만 사용될 수 있다.

🧾 주문

  • 주문 전체 흐름에 대해 원자성이 보장되어야 한다.
  • 사용 불가능하거나 존재하지 않는 쿠폰일 경우 주문은 실패해야 한다.
  • 재고가 존재하지 않거나 부족할 경우 주문은 실패해야 한다.
  • 주문 시 유저의 포인트 잔액이 부족할 경우 주문은 실패해야 한다
  • 쿠폰, 재고, 포인트 처리 등 하나라도 작업이 실패하면 모두 롤백처리되어야 한다.
  • 주문 성공 시, 모든 처리는 정상 반영되어야 한다.

🧪 동시성 테스트

  • 동일한 상품에 대해 여러명이 좋아요/싫어요를 요청해도, 상품의 좋아요 개수가 정상 반영되어야 한다.
  • 동일한 쿠폰으로 여러 기기에서 동시에 주문해도, 쿠폰은 단 한번만 사용되어야 한다.
  • 동일한 유저가 여러 기기에서 동시에 주문에도, 포인트가 중복 차감되지 않아야 한다.
  • 동일한 상품에 대해 여러 주문이 동시에 요청되어도, 재고가 정상적으로 차감되어야 한다.

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Find all relevant source files ==="
fd -e java | grep -v test | grep -v Test | sort

echo ""
echo "=== Find all test files ==="
fd -e java | grep -i test | sort

Length of output: 16085


🏁 Script executed:

#!/bin/bash
echo "=== IssuedCoupon entity ==="
fd -e java -g "*IssuedCoupon*" | grep -v test | xargs cat 2>/dev/null

echo ""
echo "=== CouponTemplate entity ==="
fd -e java -g "*CouponTemplate*" | grep -v test | xargs cat 2>/dev/null

echo ""
echo "=== CouponDiscountPolicy ==="
fd -e java -g "*CouponDiscountPolicy*" | xargs cat 2>/dev/null

echo ""
echo "=== FixedCouponDiscountPolicy ==="
fd -e java -g "*FixedCoupon*" | xargs cat 2>/dev/null

echo ""
echo "=== RateCouponDiscountPolicy ==="
fd -e java -g "*RateCoupon*" | xargs cat 2>/dev/null

Length of output: 36999


🏁 Script executed:

#!/bin/bash
echo "=== OrderApplicationService / OrderFacade ==="
fd -e java | grep -i "OrderApplicationService\|OrderFacade" | grep -v test | xargs cat 2>/dev/null

echo ""
echo "=== CouponUserService / CouponService ==="
fd -e java | grep -i "CouponService\|CouponUserService" | grep -v test | xargs cat 2>/dev/null

echo ""
echo "=== UserPoint entity ==="
fd -e java -g "*UserPoint*" | grep -v test | xargs cat 2>/dev/null

Length of output: 19288


🏁 Script executed:

#!/bin/bash
echo "=== Concurrency Tests ==="
fd -e java | grep -i "concurren\|Concurren" | xargs cat 2>/dev/null

echo ""
echo "=== IssuedCoupon Repository ==="
fd -e java -g "*IssuedCouponRepository*" | xargs cat 2>/dev/null

echo ""
echo "=== Product Repository (findByIdWithLock) ==="
fd -e java -g "*ProductRepository*" | xargs cat 2>/dev/null

echo ""
echo "=== UserPointRepository ==="
fd -e java -g "*UserPointRepository*" | xargs cat 2>/dev/null

Length of output: 38246


@looper88888: 코드 전체를 분석한 결과, 체크리스트 항목별 동작 여부는 다음과 같다.


🗞️ Coupon 도메인

# 항목 결과 근거
1 쿠폰은 사용자가 소유하고 있으며, 이미 사용된 쿠폰은 사용할 수 없어야 한다. placeOrderWithDiscount에서 coupon.getUserId().equals(userId) 소유자 검증, coupon.isUsable() 검증 수행. IssuedCoupon.use()status == USED이면 COUPON_ALREADY_USED 예외 발생.
2 쿠폰 종류는 정액 / 정률로 구분되며, 각 적용 로직을 구현하였다. ✅ (⚠️ 주의) FixedCouponDiscountPolicy, RateCouponDiscountPolicy 구현 완료 및 CouponDiscountPolicyTest로 검증됨. 단, CouponDiscountPolicyFactory와 전략 패턴 클래스들이 실제 주문 흐름에서 사용되지 않음. OrderApplicationServicetemplate.calculateDiscount()를 직접 호출하고, CouponTemplate 내부에도 if-else로 동일 로직이 중복 구현되어 있음. 전략 패턴 클래스들이 사실상 dead code 상태.
3 각 발급된 쿠폰은 최대 한번만 사용될 수 있다. findByIdWithLock (PESSIMISTIC_WRITE) + IssuedCoupon.use() 내 상태 검증. OrderConcurrencyTest.동시_쿠폰_사용_중복_방지로 동시성 정합성까지 검증됨.

🧾 주문

# 항목 결과 근거
1 주문 전체 흐름에 대해 원자성이 보장되어야 한다. placeOrderWithDiscount 전체가 단일 @Transactional로 묶여 있음.
2 사용 불가능하거나 존재하지 않는 쿠폰일 경우 주문은 실패해야 한다. 쿠폰 미존재 → COUPON_NOT_FOUND, 타인 쿠폰 → COUPON_ACCESS_DENIED, 사용 불가 → COUPON_NOT_AVAILABLE 예외 처리.
3 재고가 존재하지 않거나 부족할 경우 주문은 실패해야 한다. productRepository.findByIdWithLock + product.decreaseStock()에서 도메인 레벨 재고 검증.
4 주문 시 유저의 포인트 잔액이 부족할 경우 주문은 실패해야 한다. userPoint.use()hasEnough() 검증 → INSUFFICIENT_POINT 예외.
5 쿠폰, 재고, 포인트 처리 등 하나라도 작업이 실패하면 모두 롤백처리되어야 한다. @Transactional 롤백 보장. 주문_실패_시_쿠폰_롤백, 주문_실패_시_포인트_롤백 테스트로 검증됨.
6 주문 성공 시, 모든 처리는 정상 반영되어야 한다. 동시성 테스트의 성공 케이스에서 재고·쿠폰·포인트 최종 상태 검증됨.

🧪 동시성 테스트

# 항목 결과 근거
1 동일한 상품에 대해 여러명이 좋아요/싫어요를 요청해도, 상품의 좋아요 개수가 정상 반영되어야 한다. LikeConcurrencyTest - 동시_좋아요_정상_반영, 동시_중복_좋아요_방지, 동시_좋아요_취소_정합성 3개 케이스 검증.
2 동일한 쿠폰으로 여러 기기에서 동시에 주문해도, 쿠폰은 단 한번만 사용되어야 한다. OrderConcurrencyTest.동시_쿠폰_사용_중복_방지 - 10스레드 중 1번만 성공, 최종 USED 상태 검증.
3 동일한 유저가 여러 기기에서 동시에 주문에도, 포인트가 중복 차감되지 않아야 한다. OrderConcurrencyTest.동시_포인트_차감_정합성 - 10,000포인트에서 1,000씩 10회 → 최종 잔액 0 검증.
4 동일한 상품에 대해 여러 주문이 동시에 요청되어도, 재고가 정상적으로 차감되어야 한다. OrderConcurrencyTest.동시_재고_차감_정합성 - 재고 10개, 20스레드 → 10개만 성공, 최종 재고 0 검증.

⚠️ 운영 관점 개선 필요 사항

CouponDiscountPolicyFactory dead code 문제

전략 패턴으로 설계된 CouponDiscountPolicyFactory, FixedCouponDiscountPolicy, RateCouponDiscountPolicy실제 주문 흐름에서 전혀 사용되지 않는다. OrderApplicationServicetemplate.calculateDiscount(originalAmount)를 호출하고, 해당 메서드는 CouponTemplate 내부에서 if-else로 직접 처리한다.

// CouponTemplate.calculateDiscount() - 전략 패턴 미사용
public Money calculateDiscount(Money orderAmount) {
    if (type == CouponType.FIXED) {
        long discount = Math.min(value.amount(), orderAmount.amount());
        return new Money(discount);
    } else {
        // RATE: 직접 계산
    }
}

할인 정책이 추가될 때 CouponTemplate과 전략 클래스 양쪽에 모두 수정이 필요한 구조가 되어 유지보수 위험이 발생한다. CouponTemplate.calculateDiscount() 내부를 CouponDiscountPolicyFactory.getPolicy(type).calculateDiscount(...)로 위임하여 단일 책임을 확보하는 것을 권장한다.


🧠 Learnings used
Learnt from: Namjin-kimm
Repo: Loopers-dev-lab/loop-pack-be-l2-vol3-java PR: 152
File: apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java:29-35
Timestamp: 2026-03-06T09:19:43.842Z
Learning: In the loop-pack-be-l2-vol3-java project, the `issue()` method in `CouponService.java` (apps/commerce-api/src/main/java/com/loopers/domain/coupon/CouponService.java) uses a select-then-act pattern (existsByUserIdAndCouponTemplateId -> save) and does NOT implement concurrent duplicate issuance prevention. This is an intentional design decision for the current stage; duplicate issuance prevention (BR-C03 concurrency hardening) is deferred to a later phase.

@leeedohyun leeedohyun merged commit 627e473 into Loopers-dev-lab:dame2 Mar 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants