Skip to content

[4주차] 쿠폰 도메인, 동시성 제어, 트랜잭션 관리 - 최숙희#151

Open
SukheeChoi wants to merge 32 commits intoLoopers-dev-lab:SukheeChoifrom
SukheeChoi:volume-4
Open

[4주차] 쿠폰 도메인, 동시성 제어, 트랜잭션 관리 - 최숙희#151
SukheeChoi wants to merge 32 commits intoLoopers-dev-lab:SukheeChoifrom
SukheeChoi:volume-4

Conversation

@SukheeChoi
Copy link

@SukheeChoi SukheeChoi commented Mar 5, 2026

📌 Summary

  • 쿠폰 도메인(템플릿 + 발급) 전체 구현 및 주문-쿠폰 통합 (할인 적용 / 주문 취소 시 복원)
  • 도메인 특성별 동시성 전략 분화: 재고(비관적 락), 좋아요(UNIQUE + COUNT 파생), 쿠폰(조건부 UPDATE), 주문 취소(@Version 낙관적 락)
  • N+1 쿼리 최적화: 좋아요 수 배치 COUNT, 브랜드 삭제 시 좋아요 배치 DELETE
  • 설계 문서 4종 전면 갱신 (요구사항 / 시퀀스 / 클래스 / ERD)

🧭 Context & Decision

도메인 특성별 동시성 전략 분화

동시성 제어를 단일 전략으로 통일하지 않고, 각 도메인의 특성에 맞는 전략을 선택했습니다.

대상 전략 근거
Product 재고 비관적 락 + 도메인 엔티티 (SELECT FOR UPDATEproduct.decreaseStock()) 재고 차감은 Stock.decrease()라는 도메인 규칙(음수 방지, 수량 검증)이 존재. 이를 인프라 레이어(JPQL WHERE절)로 유출시키면 도메인 캡슐화가 깨짐
Like 좋아요 UNIQUE 제약 + COUNT(*) 파생 Product 행 잠금 자체가 불필요. 좋아요와 주문이 서로 경합하지 않음
CouponIssue 사용 조건부 UPDATE (WHERE status='AVAILABLE' AND expired_at > now) 쿠폰 상태 전이는 단순 플래그 변경이므로 별도 도메인 로직이 적음. 원자적 UPDATE가 적합
Order 취소 @Version 낙관적 락 동시 취소 빈도가 극히 낮고, "이미 취소됐으면 실패"가 자연스러운 시멘틱. 비관적 락은 과도한 방어

재고의 경우 조건부 UPDATE(SET stock = stock - :qty WHERE stock >= :qty)로 통일하는 방법도 검토했으나,
Stock.decrease()가 사실상 죽은 코드가 되어 도메인 모델의 의미가 퇴색되고,
증명된 성능 병목 없이 인프라로 로직을 이동시키는 것은 과도한 최적화로 판단하여 비관적 락을 유지했습니다.

주문 이중 취소 방지 — @Version 낙관적 락

동일 주문을 여러 스레드가 동시에 취소하면, 각각 order.cancel() 성공 후 재고를 이중 복원하는 문제가 있었습니다.
Order에 @Version을 적용하여, 첫 번째 취소만 커밋에 성공하고 나머지는 OptimisticLockingFailureException으로 트랜잭션 전체가 롤백됩니다.

비관적 락 대신 낙관적 락을 선택한 이유:

  • 같은 주문을 동시에 취소하는 시나리오는 "동일 사용자가 여러 기기에서 동시 클릭" 정도
  • 재시도가 불필요 — 취소가 이미 되었으면 다시 시도할 이유 없음
  • 경합이 없는 평상시에는 성능 영향 제로

쿠폰 조건부 UPDATE의 2단계 구조

쿠폰의 조건부 UPDATE는 affected rows = 0만으로 실패 사유(이미 사용/만료/존재하지 않음)를 구분할 수 없습니다.
사용자에게 구체적 에러 메시지를 주기 위해, 사전 SELECT로 소유자·상태·만료를 검증한 뒤 조건부 UPDATE를 최종 안전장치로 사용하는 2단계 구조를 택했습니다.

// 1단계: 사전 검증 (구체적 에러 메시지 제공)
CouponIssue issue = couponIssueRepository.findById(id);
if (!issue.getMemberId().equals(memberId)) throw FORBIDDEN;

// 2단계: 조건부 UPDATE (동시성 안전장치)
int updated = couponIssueRepository.markAsUsed(id, now);
if (updated == 0) throw BAD_REQUEST;  // race condition 방어

데드락 방지: Product ID 정렬 후 순차 락 획득

여러 상품을 동시에 잠글 때 임의 순서로 SELECT FOR UPDATE하면 교차 대기(deadlock)가 발생할 수 있습니다.
이를 방지하기 위해 Product ID 오름차순으로 정렬한 뒤 순차적으로 락을 획득합니다.

List<Long> sortedProductIds = itemRequests.stream()
    .map(OrderItemRequest::productId)
    .distinct()
    .sorted()
    .toList();

Map<Long, Product> productMap = productRepository.findAllByIdsWithLock(sortedProductIds).stream()
    .collect(Collectors.toMap(Product::getId, Function.identity()));

JPA 쿼리에서도 ORDER BY p.id ASC를 명시하여 DB 레벨에서도 정렬 순서를 보장합니다.

주문 취소 시 만료 쿠폰 복원 정책

초기에는 cancelUse() 시 무조건 AVAILABLE로 복원했으나, 고객 단순변심 취소인데
이미 만료된 쿠폰까지 되살리는 것은 비즈니스적으로 맞지 않다고 판단했습니다.

cancelUse(ZonedDateTime now)로 시그니처를 변경하여, 만료 여부를 판단한 뒤
만료됐으면 EXPIRED, 아니면 AVAILABLE로 분기 처리합니다.

public void cancelUse(ZonedDateTime now) {
    if (this.status != CouponIssueStatus.USED) {
        throw new CoreException(ErrorType.BAD_REQUEST, "사용된 쿠폰만 복원할 수 있습니다.");
    }
    this.status = isExpired(now) ? CouponIssueStatus.EXPIRED : CouponIssueStatus.AVAILABLE;
    this.usedOrderId = null;
}

Like를 UNIQUE + COUNT(*) 파생으로 전환

기존에는 Product.likeCount 컬럼을 좋아요 추가/삭제마다 갱신했습니다.
이 방식은 Product 행에 락을 걸어야 하고, 좋아요와 무관한 주문 트랜잭션까지 Product 행 경합에 휘말립니다.

**UNIQUE 제약(member_id + product_id)**으로 중복 좋아요를 DB 레벨에서 방지하고,
좋아요 수는 COUNT(*)로 파생 계산하는 방식으로 전환했습니다.
Product 행 잠금이 완전히 불필요해지고, 좋아요와 주문이 서로 경합하지 않습니다.


🏗️ Design Overview

변경 범위

카테고리 내용
신규 도메인 Coupon (템플릿), CouponIssue (발급), DiscountType (FIXED/RATE), CouponIssueStatus
주문 변경 Order에 couponIssueId / discountAmount / originalTotalPrice / @Version 추가
동시성 Product 비관적 락, CouponIssue 조건부 UPDATE, Like UNIQUE + COUNT, Order @Version
N+1 최적화 좋아요 배치 COUNT, 브랜드 삭제 시 좋아요 배치 DELETE
테스트 도메인 단위 + Facade 단위(Fake) + 동시성 통합(@SpringBootTest, 100 threads)

컴포넌트 구성

레이어 파일 역할
Domain Coupon, CouponIssue, DiscountType, CouponIssueStatus 할인 계산, 상태 전이, 자기 검증
Domain CouponRepository, CouponIssueRepository 도메인 인터페이스 (DIP)
Domain Order @Version 낙관적 락으로 이중 취소 방지
Infra CouponJpaRepository, CouponIssueJpaRepository JPA 구현 + 조건부 UPDATE
Infra ProductJpaRepository 비관적 락 (@Lock(PESSIMISTIC_WRITE))
Application CouponFacade 쿠폰 발급/조회/Admin CRUD
Application OrderFacade 쿠폰 통합 주문 생성/취소
Interfaces CouponController, CouponAdminController REST API
Interfaces ApiControllerAdvice OptimisticLockingFailureException → 409 CONFLICT

🔁 Flow Diagram

쿠폰 적용 주문 생성

sequenceDiagram
    participant C as Client
    participant OC as OrderController
    participant OF as OrderFacade
    participant PR as ProductRepository
    participant CIR as CouponIssueRepository
    participant CR as CouponRepository
    participant OR as OrderRepository

    C->>OC: POST /api/v1/users/{userId}/orders
    OC->>OF: createOrder(memberId, items, couponIssueId)

    Note over OF: @Transactional 시작

    OF->>PR: findAllByIdsWithLock(sortedProductIds)
    PR-->>OF: Products (SELECT FOR UPDATE, ID 오름차순)
    Note over OF: 브랜드 조회 + 스냅샷 생성

    loop 각 상품
        OF->>OF: product.decreaseStock(qty)
        Note over OF: Stock.decrease() — 도메인 규칙 적용
    end

    alt couponIssueId != null
        OF->>CIR: findById(couponIssueId)
        CIR-->>OF: CouponIssue (소유자/상태 검증)
        OF->>CR: findById(couponId)
        CR-->>OF: Coupon (할인 계산)
        OF->>CIR: markAsUsed(id, now) [조건부 UPDATE]
        Note over CIR: affected rows == 0 → 예외
    end

    OF->>OR: save(Order)
    Note over OF: @Transactional 커밋 (dirty checking으로 재고 반영)
    OF-->>OC: Order
    OC-->>C: 201 Created
Loading

주문 이중 취소 방지 (@Version)

sequenceDiagram
    participant T1 as Thread-1
    participant T2 as Thread-2
    participant OF as OrderFacade
    participant DB as Database

    Note over T1,T2: 동일 주문(id=1)을 동시에 취소

    T1->>OF: cancelOrder(orderId=1)
    T2->>OF: cancelOrder(orderId=1)

    T1->>DB: SELECT * FROM orders WHERE id=1 (version=0)
    T2->>DB: SELECT * FROM orders WHERE id=1 (version=0)

    Note over T1: order.cancel() + 재고 복원 + 쿠폰 복원

    T1->>DB: UPDATE orders SET status='CANCELLED', version=1 WHERE id=1 AND version=0
    DB-->>T1: affected rows = 1 ✅

    T2->>DB: UPDATE orders SET status='CANCELLED', version=1 WHERE id=1 AND version=0
    DB-->>T2: affected rows = 0 ❌ OptimisticLockException

    Note over T2: 트랜잭션 롤백 (재고·쿠폰 복원 무효화)
Loading

쿠폰 동시 사용 방지

sequenceDiagram
    participant T1 as Thread-1 (주문A)
    participant T2 as Thread-2 (주문B)
    participant DB as Database

    Note over T1,T2: 동일 CouponIssue(id=42)로 동시 주문

    T1->>DB: UPDATE coupon_issue SET status='USED' WHERE id=42 AND status='AVAILABLE'
    T2->>DB: UPDATE coupon_issue SET status='USED' WHERE id=42 AND status='AVAILABLE'

    Note over DB: Row-level lock으로 순차 실행

    DB-->>T1: affected rows = 1 ✅
    DB-->>T2: affected rows = 0 ❌

    Note over T2: CoreException → 트랜잭션 롤백
Loading

🧪 테스트 사항

테스트 검증 내용 방식
CouponTest FIXED/RATE 할인 계산, 최소주문금액 검증 단위 테스트
CouponIssueTest 사용/취소/만료/이중사용 방지, 만료 쿠폰 취소 시 EXPIRED 전이 단위 테스트
CouponFacadeTest 발급/조회/Admin CRUD, 쿠폰 적용 주문 Fake Repository
OrderFacadeTest 쿠폰 적용 주문 생성/취소, 만료 쿠폰 복원 정책 Fake Repository
StockConcurrencyTest 100건 동시 주문 → 재고 정확히 차감 @SpringBootTest + CountDownLatch
CouponUseConcurrencyTest 동일 쿠폰 100건 동시 주문 → 1건만 성공 @SpringBootTest + CountDownLatch
LikeConcurrencyTest 100명 동시 좋아요 → 정확한 count @SpringBootTest + CountDownLatch
OrderCancelConcurrencyTest 동일 주문 10건 동시 취소 → 1건만 성공, 재고 정확히 1번 복원 @SpringBootTest + CountDownLatch

📋 향후 고려사항

항목 사유
쿠폰 발급 수량 제한 현재 요구사항에 없음. 추가 시 Coupon에 maxIssueCount 필요
주문 상태 머신 (배송중/완료 등) Round 4 범위 외. 현재는 CREATED/CANCELLED만 존재
쿠폰 만료 배치 처리 getEffectiveStatus()로 조회 시점 계산하므로 배치 없이도 동작
분산 락 (Redis 등) 단일 DB 인스턴스 전제. 다중 인스턴스 시 분산 락 필요
ProductStock 분리 재고 락이 Product 조회를 블로킹하는 문제. 별도 Aggregate 분리로 해결 가능

SukheeChoi and others added 28 commits February 2, 2026 23:40
- MemberModel 엔티티 및 MemberRepository 인터페이스 추가
- MemberService: 로그인 ID 중복 검증, 비밀번호 규칙 검증, 암호화 저장
- MemberV1Controller: POST /api/v1/members API
- PasswordEncoderConfig: BCrypt 설정
- ApiControllerAdvice: @Valid 검증 예외 핸들러 추가
- 단위 테스트 6개 추가 (Service 4개, Controller 2개)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- AuthMember 어노테이션 및 AuthMemberResolver 추가
- 헤더 기반 인증 (X-Loopers-LoginId, X-Loopers-LoginPw)
- GET /api/v1/members/me API 추가
- 이름 마스킹 로직 (홍길동 → 홍길*)
- ErrorType.UNAUTHORIZED 추가
- 단위 테스트 5개 추가 (Controller 3개, DTO 2개)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberModel에 changePassword() 메서드 추가
- MemberService에 비밀번호 변경 로직 구현
  - 현재 비밀번호 검증
  - 동일 비밀번호 사용 불가
  - 비밀번호 규칙 검증 (8~16자, 영문/숫자/특수문자)
  - 생년월일 포함 불가
- MemberV1Controller에 PATCH /me/password 엔드포인트 추가
- CLAUDE.md를 .gitignore에 추가 (git 추적 제외)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberModel → Member 엔티티 리네이밍 (DDD 네이밍)
- Value Object 도입: LoginId, Email, BirthDate, Password (@embeddable, 자가 검증)
- Gender enum 추가 및 회원가입 시 성별 필수 처리
- PasswordPolicy 도메인 정책 분리 (순수 함수)
- Service 얇은 조율 계층으로 리팩토링 (검증 로직 VO/Policy로 이동)
- 포인트 조회 API 신규 구현 (GET /api/v1/points)
- AuthMemberResolver 보안 에러 메시지 통일
- 단위 테스트 (LoginIdTest, EmailTest, BirthDateTest 등)
- 통합 테스트 (MemberServiceIntegrationTest, @SpyBean)
- E2E 테스트 (MemberV1ApiE2ETest, PointV1ApiE2ETest)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
DDD 리팩토링 + Value Object 도입 + 포인트 조회 구현
기능 요구사항에 해당하지 않는 Gender enum, 포인트 조회 API를 제거한다.
Member 엔티티에서 gender/point 필드를 제거하고 관련 테스트를 정리한다.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 01-requirements.md: 액터 정의, 미결정 사항 섹션 추가
- 02-sequence-diagrams.md: 트랜잭션 경계 rect 블록, 읽는 법, 잠재 리스크 추가
- 03-class-diagram.md: 다이어그램 읽는 법, 잠재 리스크 추가
- 04-erd.md: 잠재 리스크 섹션 추가

브랜드/상품/좋아요/주문 도메인의 요구사항, 시퀀스, 클래스, ERD 설계 완료

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
판단 기준을 명확히 함: "주문 상세 화면을 독립적으로 렌더링할 수 있는가?"
- 필수/권장/제외 항목 분류 및 근거 추가
- image_url 제외 이유 명시 (현재 상품 스펙에 없음, 오버엔지니어링 방지)
- 트레이드오프 설명 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 주문 취소 API 명세 추가 (POST /api/v1/orders/{orderId}/cancel)
- 대고객 브랜드 목록 API 추가 (GET /api/v1/brands)
- order_item 삭제 정책 수정 (Order와 생명주기 공유)
- 시퀀스 다이어그램 URI prefix 통일 (/api-admin/v1)
- 상품 삭제 유스케이스 추가 (US-P05)
- 좋아요 목록 N+1 의도 명시
- 주문 취소 시 삭제된 상품 처리 리스크 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 주문 취소 API 제거 (요구사항에 없음)
- US-O04 유스케이스 제거
- 시퀀스 다이어그램 8번 (주문 취소) 제거
- 대고객 브랜드 목록 API 제거 (요구사항에 없음)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 도메인 & 객체 설계 전략 (Entity/VO/Domain Service 구분 기준)
- 아키텍처 & 패키지 전략 (DIP 실무 타협 기준, 의존 방향)
- DIP 인사이트: 정석 vs 실무 타협 정리 (DDD 저자 명언 포함)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- domain/member/MemberService → application/member/MemberFacade
- 레이어드 아키텍처 원칙에 맞게 유스케이스 조율을 Application Layer에서 담당
- Controller, 통합 테스트 import 경로 수정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Brand: Entity + CRUD (삭제 시 연관 상품 cascade soft delete)
- Product: Entity + Price/Stock VO + CRUD (N+1 해결: JPQL JOIN)
- Like: Entity (hard delete) + 좋아요 등록(멱등)/취소
- Order: Aggregate Root + OrderItem 스냅샷 + 재고 차감
- DIP 적용: Repository Interface(Domain) ← Impl(Infrastructure)
- ProductWithBrand 조회 전용 모델로 읽기/쓰기 관심사 분리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- VO 테스트: Price, Stock (생성 검증, 비즈니스 규칙)
- Entity 테스트: Brand, Product, Order, OrderItem, Like
- Facade 테스트: Fake Repository 기반 순수 단위 테스트
  - BrandFacadeTest, ProductFacadeTest, LikeFacadeTest, OrderFacadeTest
- DIP 이점 활용: Spring 컨텍스트 없이 도메인 로직 검증

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P0 요구사항 미구현 수정:
- 브랜드/상품 삭제 시 좋아요 hard delete 연쇄 처리
- 주문 취소 + 재고 복원 API 추가 (POST /orders/{id}/cancel)
- 좋아요 목록/주문 상세 조회 권한 검증 추가 (FORBIDDEN)
- 좋아요 취소 멱등성 보장 (예외 → 조기 리턴)

P1 설계 결함 수정:
- 상품 목록 정렬 지원 (latest/price_asc/likes_desc)
- findByIdWithBrand LEFT JOIN 조건 버그 수정

P2 테스트 품질 보강:
- Fake Repository soft delete 필터링 반영
- 주문 취소, 연쇄 삭제, 멱등성 등 누락 테스트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Order.ItemSnapshot record 도입, Order.create()가 스냅샷을 받아 내부에서 OrderItem 생성
- OrderItem 생성자를 package-private로 변경하여 외부 패키지에서 직접 생성 차단
- OrderFacade는 더 이상 OrderItem을 직접 생성하지 않고 ItemSnapshot만 전달

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 레이어드 아키텍처 Mermaid 다이어그램 추가 (Facade별 책임 명시)
- 전체 클래스 다이어그램 갱신 (ItemSnapshot, package-private 반영)
- Aggregate 라이프사이클 통제 점검 결과 및 Entity vs VO 통제 기준 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
findByIdWithBrand() JOIN 쿼리를 제거하고, ProductFacade에서
Product와 Brand를 각각 조회 후 조합하도록 리팩토링.
목록 조회는 성능을 위해 기존 JOIN 방식 유지.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Order.create()에 빈 주문 방지 guard 추가 (도메인 불변식)
- Price, Stock에 @EqualsAndHashCode 추가 (VO 값 동등성 보장)
- OrderTest에 빈 항목/null 항목 테스트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 상품 일괄 비관적 락 쿼리 도입 (DB 라운드트립 N→1)
- 데드락 방지를 위한 락 순서 보장 및 동시성 테스트 추가
- Order 금액 불변식 검증 (discountAmount <= originalTotalPrice)
- Clock 주입으로 도메인 시간 의존성 제거 (테스트 안정성)
- 쿠폰 주문 연동 로직을 CouponFacade로 위임 (OrderFacade 의존성 5→4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
좋아요 — Product.likeCount 제거, UNIQUE 제약 + COUNT(*) 파생으로 락 불필요 구조 전환
쿠폰 — 비관적 락 제거, 조건부 UPDATE(markAsUsed) + affected rows 검증으로 원자적 상태 전이
재고 — 비관적 락 유지 (다중 자원 원자성 + 높은 경합 특성)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BrandFacade.deleteBrand(): 좋아요 루프 삭제 → deleteAllByProductIdIn() 배치 삭제
- ProductFacade.enrichWithLikeCount(): 개별 COUNT → countByProductIds() GROUP BY 배치 조회
- LikeRepository/LikeJpaRepository: 배치 메서드 추가
- LikeRepositoryImpl: Object[] → Map 변환 위임 구현
- FakeLikeRepository: 테스트용 배치 구현 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 01-requirements: 쿠폰 유저스토리, 동시성 전략, 쿠폰 API 추가
- 02-sequence-diagrams: 주문 생성(비관적 락+쿠폰), 좋아요(UNIQUE 기반), 쿠폰 발급/주문 취소 신규 추가
- 03-class-diagram: Coupon Aggregate 추가, Product likeCount 제거, Order 쿠폰 필드 추가
- 04-erd: coupon/coupon_issue 테이블, DDL, FK 정책, 잠재 리스크 갱신

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 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: 4cb0f9d9-89af-42b4-a704-f840b6a4f228

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

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.

고객 단순변심 취소 시 이미 만료된 쿠폰까지 AVAILABLE로 복원되던 문제 수정.
cancelUse(ZonedDateTime now)로 시그니처 변경하여 만료 여부를 판단한 뒤,
만료됐으면 EXPIRED, 아니면 AVAILABLE로 분기 처리.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SukheeChoi and others added 3 commits March 6, 2026 06:54
비관적 락 + 엔티티 변경(read-modify-write) 방식에서
조건부 UPDATE(SET stock = stock - :qty WHERE stock >= :qty)로 전환.
쿠폰(조건부 UPDATE), 좋아요(UNIQUE + COUNT)와 함께
모든 동시성 제어가 read-modify-write를 피하는 구조로 통일됨.

동시성 테스트 threadCount를 10 → 100으로 강화.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
조건부 UPDATE는 도메인 로직(Stock.decrease)을 인프라 레이어로
유출시키므로, 증명된 병목 없이 적용하는 것은 과도한 최적화로 판단.
비관적 락 + product.decreaseStock() 패턴으로 되돌려 도메인 캡슐화 유지.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
동일 주문을 동시에 취소하면 재고가 이중 복원되는 문제를 방지.
동시 취소는 빈도가 낮고 "이미 취소됐으면 실패"가 자연스러운
시멘틱이므로, 비관적 락 대신 @Version 낙관적 락을 채택.
OptimisticLockingFailureException → 409 CONFLICT 응답 핸들러 추가.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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