Skip to content

[4주차] 쿠폰 신규 도메인 개발, 동시성 제어 및트랜잭션 관리 - 안성훈#171

Open
shAn-kor wants to merge 17 commits intoLoopers-dev-lab:shAn-korfrom
shAn-kor:volume4
Open

[4주차] 쿠폰 신규 도메인 개발, 동시성 제어 및트랜잭션 관리 - 안성훈#171
shAn-kor wants to merge 17 commits intoLoopers-dev-lab:shAn-korfrom
shAn-kor:volume4

Conversation

@shAn-kor
Copy link

@shAn-kor shAn-kor commented Mar 6, 2026

📌 Summary

배경: 커머스 도메인에서 재고 변경, 쿠폰 사용, 좋아요 변경 등 동시 요청 시 정합성 문제가 발생할 수 있습니다.

목표: 각 도메인의 동시성 리스크를 분석하고, 리스크 수준과 경합 특성에 맞는 동시성 제어 전략을 적용하여 데이터 정합성을 보장합니다.

결과: 3개 도메인에 맞는 동시성 전략을 적용하고, CountDownLatch + ExecutorService 기반 동시성 통합 테스트로 검증을 완료했습니다. 쿠폰 도메인 신규 구현 및 주문-쿠폰 연동도 포함됩니다.

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: OrderFacade가 재고/쿠폰/취소 조합 로직을 흩어서 보유하고 있었고, 쿠폰 사용은 엔티티 상태 직접 변경 방식이라 동시성 경합 시 정합성 보장이 불명확했다.
  • 문제(또는 리스크):
    • 쿠폰 동시 사용 시 AVAILABLE → USED 상태 전이가 비원자적이라 중복 사용 가능성이 있었다.
    • Long PK를 그대로 API에 노출해 순서 추측이 가능했다.
    • 재고 전략(비관락/낙관락/원자 업데이트) 비교 근거가 테스트 수치 없이 코드에만 존재했다.
  • 성공 기준(완료 정의):
    • 동일 쿠폰에 대한 동시 사용 요청 N건 중 정확히 1건만 성공한다.
    • 주문 취소 → 재고 복구 + 쿠폰 복구가 단일 유즈케이스(OrderUseCase.cancel)에서 트랜잭션으로 처리된다.
    • 모든 도메인 엔티티 PK가 UUID BINARY(16)이며, API 응답도 UUID string으로 직렬화된다.
    • 쿠폰/주문/상품 도메인 전체에 단위·통합·E2E·동시성 테스트가 존재한다.

선택지와 결정

  • 고려한 대안:
    • A: OrderFacade를 유지하면서 쿠폰 원자 업데이트만 추가 — Facade 로직 분산 문제가 해결되지 않음
    • B: OrderUseCase로 통합, OrderFacade 제거, 쿠폰·재고·좋아요 모두 원자 업데이트로 확정
  • 최종 결정: BOrderUseCase로 단일 오케스트레이션, Facade는 BrandAdminFacade·LikeFacade·ProductQueryFacade 등 순수 조합 목적만 잔존
  • 트레이드오프:
    • 원자 업데이트는 단일 상태 전이에 고성능이나, 다단계 상태 머신 확장 시 별도 락/보상 설계 필요
    • 낙관락은 저경합 환경에서 유리하나 고경합 시 재시도 비용 증가 (ADR 수치: 원자 업데이트 266/s vs 낙관락 49/s at 20 req/10 stock)
  • 추후 개선 여지: 재고 전략 반복 측정(워밍업 + N회 평균/분산), 쿠폰 발급(issue) 경로 원자화 여부 결정
  • 비고
    • 가능하면 유스케이스 사용 대신 작은 트랜잭션 단위로 처리하려 했습니다. 그러나 실패시 복구 로직 고려와 그 복구 로직이 실패할때의 처리 방안 등 이번 과제 말고도 고민할 것이 많아져 이번주차에서는 유스케이스 사용 및 한 트랜잭션으로 주문 로직을 처리했습니다.

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: apps/commerce-api (전체 도메인), modules/jpa (BaseEntity UUID), modules/redis (Testcontainers)
  • 신규 추가:
    • OrderUseCase — 주문 생성·취소 오케스트레이션 (재고 예약 + 쿠폰 사용 + 취소 복구)
    • CouponApplicationService — 쿠폰 사용/사용취소 원자 업데이트
    • CouponAdminApplicationService — 쿠폰 생성·발급 어드민 유즈케이스
    • ProductStockApplicationService — 재고 3전략(원자/낙관/비관) 메서드 분리
    • docs/adr/2026-03-05-stock-deduction-concurrency-adr.md — 동시성 전략 ADR
    • docs/sql/schema-uuid-binary16.sql — UUID 스키마 DDL
  • 제거/대체:
    • OrderFacadeOrderUseCase로 대체 (크로스도메인 조합 책임 이전)

주요 컴포넌트 책임

  • OrderUseCase: 주문 생성(재고 예약 → 쿠폰 사용 → 주문 저장) 및 취소(주문 상태 → 재고 복구 → 쿠폰 복구) 트랜잭션 오케스트레이션
  • CouponApplicationService: markUsedAtomically / markAvailableAtomically — 조건부 JPQL update, 0 row = CONFLICT(409)
  • ProductStockApplicationService: decreaseStockAtomically / decreaseStockWithOptimisticLock / decreaseStockWithPessimisticLock 전략 노출
  • IssuedCouponJpaRepository: @Modifying 조건부 update 쿼리 (AVAILABLE→USED, USED→AVAILABLE)
  • BaseEntity (modules/jpa): @Id 타입을 UUID로 변경, @Column(columnDefinition = "BINARY(16)") 적용

🔁 Flow Diagram

Main Flow — 주문 생성 (쿠폰 적용)

sequenceDiagram
  autonumber
  participant Client
  participant OrderController
  participant OrderUseCase
  participant ProductStockAppSvc
  participant CouponAppSvc
  participant OrderAppSvc
  participant DB

  Client->>OrderController: POST /orders (productId, couponId?)
  OrderController->>OrderUseCase: create(CreateOrderCommand)
  OrderUseCase->>ProductStockAppSvc: reserveStock(productId, qty)
  ProductStockAppSvc->>DB: UPDATE stock SET stock = stock - qty WHERE stock >= qty
  DB-->>ProductStockAppSvc: affected rows (0 = 품절 → 409)
  alt couponId 있음
    OrderUseCase->>CouponAppSvc: use(UseCouponCommand)
    CouponAppSvc->>DB: UPDATE issued_coupon SET status='USED' WHERE id=? AND status='AVAILABLE'
    DB-->>CouponAppSvc: affected rows (0 = 이미 사용 → 409)
  end
  OrderUseCase->>OrderAppSvc: save(order)
  OrderAppSvc->>DB: INSERT order
  DB-->>OrderUseCase: saved order
  OrderUseCase-->>OrderController: OrderView
  OrderController-->>Client: 201 Created
Loading

Cancel Flow — 주문 취소 (재고 + 쿠폰 복구)

sequenceDiagram
  autonumber
  participant Client
  participant OrderController
  participant OrderUseCase
  participant OrderAppSvc
  participant ProductStockAppSvc
  participant CouponAppSvc
  participant DB

  Client->>OrderController: DELETE /orders/{orderId}
  OrderController->>OrderUseCase: cancel(orderId, memberId)
  OrderUseCase->>OrderAppSvc: cancel(orderId, memberId)
  OrderAppSvc->>DB: UPDATE order SET status='CANCELLED'
  DB-->>OrderAppSvc: CancelledOrder (재고qty, couponId, 소유 memberId)
  OrderUseCase->>ProductStockAppSvc: restoreStock(productId, qty)
  ProductStockAppSvc->>DB: UPDATE stock SET stock = stock + qty
  alt 쿠폰 사용 이력 있음
    OrderUseCase->>CouponAppSvc: cancelUse(couponId, memberId)
    CouponAppSvc->>DB: UPDATE issued_coupon SET status='AVAILABLE' WHERE status='USED'
  end
  OrderUseCase-->>OrderController: void
  OrderController-->>Client: 204 No Content
Loading

✅ 테스트 커버리지

신규 테스트 목록

분류 테스트 클래스 설명
도메인 단위 IssuedCouponDomainTest 쿠폰 상태 전이 규칙
도메인 단위 CouponDomainTest 쿠폰 발급 가능 여부
통합 CouponApplicationServiceIntegrationTest 쿠폰 사용/취소 통합
통합 CouponAdminApplicationServiceIntegrationTest 쿠폰 생성·발급 통합
통합 OrderUseCaseIntegrationTest 주문 취소 + 쿠폰/재고 복구 시나리오
E2E CouponApiE2ETest 쿠폰 API 전체 흐름
동시성 CouponUseConcurrencyTest 쿠폰 동시 사용 — 1건만 성공
동시성 ProductStockApplicationServiceConcurrencyTest 재고 3전략 비교 + 좋아요 혼합

재고 동시성 지표 (20 req / 10 stock)

전략 성공 품절 충돌 처리량(req/s)
ATOMIC UPDATE 10 10 0 266.69
PESSIMISTIC 10 10 0 184.27
OPTIMISTIC (retry 5) 10 3 7 49.66

📎 관련 문서 / 커밋

커밋 목록 (volume4, 17 commits on top of 7fe2ff7)

279df31 test: UUID/memberId 모델 변경에 맞춰 동시성·도메인 테스트를 정합화한다
2ff8e60 fix: 주문-쿠폰 연계 계약을 memberId 기반으로 정합화한다
04cb566 fix: 원자적 재고/좋아요 업데이트 경로와 주문 컨트롤러 의존성을 정합화한다
02b1fc9 docs: 재고 차감 동시성 ADR을 추가한다
f409f31 test: 재고 낙관락 재시도 지표를 업데이트한다
071ee8b docs: 주문·쿠폰 설계문서와 체크리스트 정합성 업데이트
5ab62e0 test: application 서비스 테스트를 통합 테스트로 분리한다
d8ea265 test: 쿠폰·주문 취소 통합 시나리오를 보강한다
15ecbdf test: 쿠폰 발급·사용 동시성 테스트 추가
e487c3f test: 쿠폰 API E2E 테스트 추가
26ccff2 test: 쿠폰 애플리케이션·컨트롤러 통합 테스트 추가
920301e test: 쿠폰 도메인 단위 테스트 추가
19ec48c test: 상품 재고 차감 동시성 테스트 추가
0d45893 test: 상품 좋아요 동시성 테스트 추가
44f249d test: Testcontainers 설정을 ImportTestcontainers 패턴으로 정리
db7160c feat: 쿠폰 어드민 API와 주문 유즈케이스를 통합한다
5f2b4dc refactor: UUID BINARY(16) 식별자 체계로 전환

관련 문서

@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: 673024c0-b2ea-4f95-956b-dbd8643b24cf

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.

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.

1 participant