[volume-4] 쿠폰 도메인 구현 및 동시성 제어#160
[volume-4] 쿠폰 도메인 구현 및 동시성 제어#160letter333 wants to merge 134 commits intoLoopers-dev-lab:letter333from
Conversation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- member-erd.md: 회원 테이블 ERD 설계 - member-signup-design.md: 시퀀스/클래스 다이어그램, 패키지 구조 - CLAUDE.md: 개발 규칙 및 문서 작성 가이드 반영 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Member 도메인 객체 구현 (순수 Java, JPA 어노테이션 없음) - 필드 검증: loginId, password, name, birthday, email - 비밀번호 규칙: 8~16자, 영문+숫자+특수문자, 생년월일 포함 불가 - encryptPassword()로 암호화된 비밀번호 교체 지원 test: add MemberTest with 14 unit test cases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ure/member-signup
… entity Member 도메인의 Infrastructure 레이어 구현: - MemberEntity (JPA 영속성 엔티티, Domain↔Entity 변환) - MemberRepository 인터페이스 (도메인 레이어) - MemberJpaRepository (Spring Data JPA) - MemberRepositoryImpl (Repository 구현체) - MemberEntityTest, MemberRepositoryImplIntegrationTest - spring-security-crypto 의존성 추가 - docker-java.properties (Docker Engine 29 TestContainers 호환) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberService.signUp(): 중복 검사 → 도메인 생성 → 비밀번호 암호화 → 저장 - PasswordEncoderConfig: BCryptPasswordEncoder Bean 등록 - MemberServiceTest: 정상 가입, loginId 중복, email 중복 통합 테스트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberInfo: Domain → 응답 변환 record (password, birthday 제외) - MemberFacade: MemberService 위임 및 MemberInfo 변환 - MemberInfoTest, MemberFacadeTest 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- POST /api/v1/members → 201 Created - MemberV1Dto: SignUpRequest/SignUpResponse record - MemberV1ApiSpec: Swagger 스펙 인터페이스 - MemberV1Controller: Facade 위임 및 응답 변환 - MemberV1ApiE2ETest: 정상 가입(201), 검증 실패(400), 중복(409) E2E 테스트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Controller에서 birthday null/빈 문자열/잘못된 형식 시 400 BAD_REQUEST 반환 - birthday 관련 E2E 테스트 2건 추가 - 회원가입 API .http 파일 생성 - CLAUDE.md 프로젝트 규칙 보강 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 내 정보 조회 기능 시퀀스/클래스 다이어그램 작성 - 회원가입 시퀀스 다이어그램 Entity 반환 화살표 누락 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberRepository 인터페이스에 findByLoginId 추가 - MemberJpaRepository, MemberRepositoryImpl 구현 - 통합 테스트 2건 추가 (존재/미존재 케이스) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberService.authenticate() 메서드 추가 (loginId 조회 + 비밀번호 검증) - ErrorType에 UNAUTHORIZED(401) 추가 - 통합 테스트 3건 추가 (성공, 회원 미존재, 비밀번호 불일치) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- GET /api/v1/members/me 엔드포인트 추가 (X-Loopers-LoginId, X-Loopers-LoginPw 헤더 인증) - MemberInfo에 birthday 필드 추가, MyInfoResponse DTO 추가 - MemberFacade.getMyInfo(), MemberV1ApiSpec, MemberV1Controller 구현 - ApiControllerAdvice에 MissingRequestHeaderException 핸들러 추가 - E2E 테스트 4건 추가 (200, 401×2, 400) - 통합 테스트 생성자 주입으로 리팩터링 (필드 주입 → 생성자 주입) - member-v1.http에 내 정보 조회 요청 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- password MIN(8자), MAX(16자) 성공 테스트 추가 - name MIN(한글 2자), MAX(한글 20자) 성공 테스트 추가 - birthday 오늘 날짜(경계값) 성공 테스트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feature/member-profile-lookup
Member.changePassword() 메서드를 통해 현재 비밀번호 검증, 동일 비밀번호 방지, 새 비밀번호 룰 검증(길이/패턴/생년월일), 암호화까지 도메인 엔티티에서 캡슐화 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
MemberService.updatePassword() 추가, MemberRepository에 updatePassword 메서드 정의, MemberRepositoryImpl에서 JPA dirty checking 기반 UPDATE 구현, 통합 테스트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
PATCH /api/v1/members/me/password 엔드포인트 추가, 헤더 PW와 Body currentPassword 일치 검증, E2E 테스트 8건 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
API 응답 규칙, 의존성 방향, 인증 헤더 규칙, TDD 단계별 진행 규칙, 테스트 경계값 케이스 가이드 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: 비밀번호 변경 API 구현
도메인 계층의 PasswordEncoder 의존성을 제거하고, 유즈케이스 검증(현재 비밀번호 확인, 동일 비밀번호 확인)을 MemberService로 이동하여 의존성 방향 규칙 준수 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
refactor: 비밀번호 변경 검증 책임을 서비스 레이어로 분리
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…loop-pack-be-l2-vol3-java into feature/member-signup
feat: 회원 도메인 기능 구현 (가입, 조회, 비밀번호 변경)
MemberInfo에 withMaskedName() 메서드 추가하여 이름의 마지막 글자를 *로 마스킹하는 기능 구현 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: 내 정보 조회 시 이름 마스킹 기능 추가
LOGIN_ID_PATTERN을 추가하여 영문 대소문자와 숫자만 허용 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 기본 배송비 3,000원, 50,000원 이상 주문 시 무료배송 - totalAmount 기준으로 배송비 판단 (쿠폰 할인 적용 전) - 배송비 테스트 6개 케이스 추가 (경계값 포함) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: 배송비 자동 계산 기능 구현
동일한 시그니처의 메서드가 중복 정의되어 컴파일 에러가 발생하는 문제 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- MemberCoupon 생성자에 Coupon 파라미터 추가 - setCoupon() 메서드 제거로 도메인 객체 불변성 강화 - CouponFacade.issueCoupon()에서 중복 Coupon 조회 제거 (DB 쿼리 1회 감소) - MemberCouponEntity.toDomainWithCoupon()에서 생성자로 Coupon 전달 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- OrderFacade에서 CouponFacade 의존을 MemberCouponService 직접 의존으로 교체하여 Facade→Facade 의존 제거 및 MemberCoupon 중복 DB 조회(2회→1회) 해소 - getMyCoupons API에 Page 기반 페이지네이션 추가 및 count 쿼리 분리 - Order.removeCouponDiscount() 미사용 데드 코드 제거 - CouponFacade에서 주문 전용 메서드 3개 제거 (calculateCouponDiscount, applyCoupon, cancelCouponUsage) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Domain Service 4개: MemberService, BrandService, CategoryService, ProductService → @service - Infrastructure Repository 4개: MemberRepositoryImpl, BrandRepositoryImpl, CategoryRepositoryImpl, ProductRepositoryImpl → @repository Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BrandInfo에 likeCount 필드가 존재하지만 BrandResponse DTO에 매핑되지 않아 API 응답에서 좋아요 수가 누락되던 문제 수정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CategoryRepository, CategoryJpaRepository, CategoryRepositoryImpl에서 호출처 없는 findAllActiveByParentId() 메서드 삭제 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ID 포함 생성자(DB 복원용)에서 validateBirthday() 호출 누락 수정 - 인코딩된 비밀번호에 대한 validatePasswordNotContainsBirthday()는 복원 경로에서 의미 없으므로 제외 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DB에서 복원되는 데이터는 저장 시 이미 검증된 값이므로 매 조회마다 LocalDate.now() 호출하는 오버헤드 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- @component → @Service/@repository 어노테이션 일관성 통일 (coupon 도메인) - getMyCoupons() 3개 COUNT 쿼리를 SUM+CASE 1개 통합 쿼리로 최적화 - getIssuableCoupons() 전체 ID 메모리 로딩을 DB LEFT JOIN 쿼리로 대체 - OrderFacade 쿠폰 검증/할인 계산 로직을 MemberCouponService로 추출 - 미사용 import 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 통합 쿼리(getStatusCounts)로 대체된 개별 COUNT 메서드 3건 제거 - LEFT JOIN 쿼리로 대체된 getIssuedCouponIds, findAllIssuable 체인 제거 - MemberCouponListInfo 미사용 import 제거 - validateAndCalculateDiscount → validateAndGetCoupon으로 변경하여 OrderFacade에서 MemberCoupon 재조회 없이 쿠폰 사용 처리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
refactor: 코드 품질 개선 및 데드 코드 정리
SELECT ... FOR UPDATE 네이티브 쿼리로 상품 row를 잠근 후 재고를 변경하여 동시 주문 시 Lost Update로 인한 과매도(overselling)를 방지한다. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Product: 기존 재고용 비관적 락(findByIdForUpdate) 재사용하여 likeCount 증감 시 Lost Update 방지 - Brand: @Version 낙관적 락 + TransactionTemplate 재시도(최대 3회)로 동시성 문제 해결 - BrandEntity에 version 필드 추가, BrandRepository에 likeCount 전용 메서드 추가 - ApiControllerAdvice에 ObjectOptimisticLockingFailureException 핸들러 추가 - Product/Brand 각각 동시성 테스트(10 스레드) 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
useCoupon 호출 시 SELECT FOR UPDATE로 row-level lock을 획득하여 동시 사용 요청에서 정확히 1건만 성공하도록 보장한다. 원자적 UPDATE 대신 비관적 락을 선택한 이유: - OrderFacade 구조상 할인 계산을 위한 SELECT 생략 불가 - 도메인 객체(MemberCoupon.use())에서 에러 세분화 유지 - 프로젝트 전체 동시성 패턴과 일관성 확보 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
주문 취소 시 재고 복구·쿠폰 반환 등 부수 효과의 이중 실행을 방지하기 위해 Order에 비관적 락(SELECT FOR UPDATE)을 적용하고, 단일 locking read 패턴으로 MySQL REPEATABLE READ 환경에서의 stale read 문제를 해결한다. - OrderJpaRepository: @lock(PESSIMISTIC_WRITE) 적용 JPQL 추가 - OrderService: getOrderForUpdate, saveOrder 메서드 추가 - OrderFacade: cancelOrder, changeOrderStatusForAdmin 비관적 락 기반으로 변경 - 단위 테스트 수정 및 동시성 통합 테스트(OrderCancelConcurrencyTest) 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
createOrder, cancelOrder, changeOrderStatusForAdmin에서 Product 락을 클라이언트 요청 순서대로 획득하던 것을 productId 오름차순으로 정렬하여 순환 대기(Circular Wait) 조건을 원천 차단한다. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 락 없이 취소하는 위험 경로(OrderService.cancelOrder) 제거 - 취소는 OrderFacade에서 비관적 락 + 재고복원 + 쿠폰취소를 포함한 완전한 흐름으로만 실행 - 주문 상태 변경 동시성 테스트 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: 동시성 문제 해결 — 비관적 락 및 데드락 방지 적용
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- MemberCouponRepository에서 미사용 existsByMemberIdAndCouponId 제거 - LikeFacadeTest의 transactionTemplate 모킹을 mockTransactionTemplate()으로 추출 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <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 |
- Product/Brand의 like_count 증감을 낙관적 락 + 엔티티 수정 방식에서 네이티브 SQL(like_count + 1) 원자적 UPDATE로 변경 - LikeFacade의 브랜드 좋아요에서 TransactionTemplate 재시도 로직 제거 - LikeFacadeConcurrencyTest 신규 추가: 10명 동시 좋아요/취소 시 like_count 정합성 검증 - ProductLikeCountPersistenceTest 신규 추가: DB 반영 검증 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- MemberCouponEntity에 @Version 필드 추가로 낙관적 락 적용 - 비관적 락(lockById, findByIdForUpdate) 제거 → findByIdWithCoupon 사용 - OrderFacade.createOrder()에 @retryable 추가 (OptimisticLock 실패 시 최대 3회 재시도) - spring-retry 의존성 및 @EnableRetry 설정 추가 - 동시성 테스트에서 ObjectOptimisticLockingFailureException 처리 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4주차에 대한 커밋만 보시려면 이거로 보시는게 더 좋을 것 같습니다.
📌 Summary
🧭 Context & Decision
문제 정의
선택지와 결정
동시성 전략
MemberCoupon 낙관적 락 + Facade 재시도
쿠폰 중복 발급 방지
findByIdForUpdate로 수량 체크 후,(memberId, couponId)UNIQUE 제약으로 2차 방어데드락 방지
productId오름차순 정렬로 락 순서 고정🏗️ Design Overview
변경 범위
OrderService.cancelOrder()→OrderFacade로 일원화, dead code 제거주요 컴포넌트 책임
쿠폰 도메인
Coupon: 쿠폰 생성/수정/삭제, 할인 계산(FIXED/RATE), 발급 수량 관리MemberCoupon: 발급된 쿠폰 상태 관리(AVAILABLE→USED→AVAILABLE), 사용/취소CouponValidator: 쿠폰 생성/수정 시 비즈니스 규칙 검증 (유틸리티 클래스)CouponFacade: Admin CRUD, 사용자 발급/조회 오케스트레이션CouponAdminV1Controller/CouponV1Controller: REST API 엔드포인트동시성 제어
ProductService.decreaseStock/increaseStock: ID-only 비관적 락 → 재고 변경MemberCouponService.issueCoupon: 비관적 락 + UNIQUE 제약 이중 방어MemberCouponService.useCoupon: @Version 낙관적 락으로 이중 사용 방지 (비관적 락에서 전환)OrderFacade.createOrder: @retryable로 낙관적 락 실패 시 전체 재시도 (재고 차감 포함 롤백 후 새 TX)OrderFacade.cancelOrder/changeOrderStatusForAdmin: 주문 행 비관적 락 → 재고 복구 + 쿠폰 취소주문 개선
Order: 상태 머신(canCancel/canTransitionTo), 배송비 자동 계산(5만원 이상 무료)OrderFacade.restoreStockAndCancelCoupon(): 취소 시 재고+쿠폰 복구 공통 메서드🔁 Flow Diagram
쿠폰 발급 Flow (동시성 제어 포함)
sequenceDiagram autonumber participant Client participant CouponFacade participant MemberCouponService participant CouponRepository participant MemberCouponRepository participant DB Client->>CouponFacade: POST /api/v1/coupons/{id}/issue CouponFacade->>MemberCouponService: issueCoupon(couponId, memberId) MemberCouponService->>CouponRepository: findByIdForUpdate(couponId) CouponRepository->>DB: SELECT id FOR UPDATE DB-->>CouponRepository: locked row MemberCouponService->>MemberCouponService: coupon.issue() (수량 체크 + 증가) MemberCouponService->>CouponRepository: save(coupon) MemberCouponService->>MemberCouponRepository: save(memberCoupon) Note over MemberCouponRepository,DB: UNIQUE(memberId, couponId) 중복 방어 MemberCouponRepository-->>MemberCouponService: saved MemberCouponService-->>CouponFacade: MemberCoupon CouponFacade-->>Client: 발급 완료주문 생성 Flow (낙관적 락 + @retryable 재시도)
sequenceDiagram autonumber participant Client participant OrderFacade participant ProductService participant MemberCouponService participant OrderService participant DB Client->>OrderFacade: POST /api/v1/orders Note over OrderFacade: @Retryable (max 3회, ObjectOptimisticLockingFailureException) Note over OrderFacade: @Transactional (재시도마다 새 TX) loop 상품별 (productId 오름차순) OrderFacade->>ProductService: decreaseStock(productId, optionId, qty) ProductService->>DB: SELECT id FOR UPDATE → 재고 차감 end alt 쿠폰 사용 요청 있음 OrderFacade->>MemberCouponService: useCoupon(memberCouponId) MemberCouponService->>DB: SELECT (낙관적 락, @Version 체크) alt 버전 충돌 DB-->>MemberCouponService: ObjectOptimisticLockingFailureException MemberCouponService-->>OrderFacade: 예외 전파 Note over OrderFacade: TX 롤백 (재고 차감 포함) → 재시도 else 쿠폰 이미 USED MemberCouponService-->>OrderFacade: CoreException("이미 사용된 쿠폰") Note over OrderFacade: 재시도 중단 (비즈니스 예외) else 성공 MemberCouponService->>DB: UPDATE status=USED, version+1 end end OrderFacade->>OrderService: createOrder(order) OrderService->>DB: INSERT order OrderFacade-->>Client: 주문 완료주문 취소 Flow
sequenceDiagram autonumber participant Client participant OrderFacade participant OrderService participant ProductService participant MemberCouponService participant DB Client->>OrderFacade: DELETE /api/v1/orders/{id} OrderFacade->>OrderService: getOrderForUpdate(orderId) OrderService->>DB: SELECT ... FOR UPDATE (주문 락) OrderFacade->>OrderFacade: order.cancel() loop 상품별 (productId 오름차순) OrderFacade->>ProductService: increaseStock(productId, optionId, qty) ProductService->>DB: SELECT id FOR UPDATE → 재고 복구 end OrderFacade->>MemberCouponService: cancelCouponUsage(orderId) OrderFacade->>OrderService: saveOrder(order) OrderFacade-->>Client: 취소 완료