Skip to content

feat/118 :: Waiting 웨이팅 서비스 구현#26

Open
lian2945 wants to merge 121 commits into
mainfrom
feat/118
Open

feat/118 :: Waiting 웨이팅 서비스 구현#26
lian2945 wants to merge 121 commits into
mainfrom
feat/118

Conversation

@lian2945

Copy link
Copy Markdown
Collaborator

📌 관련 이슈

  • close #118

📝 변경 사항 요약

작업 유형

  • ✨ feat: 새로운 기능 추가 (기능 단위 완료 후 PR)
  • 🐛 fix: 버그 수정 (버그 1개 = PR 1개)
  • ♻️ refactor: 코드 리팩토링 (작업 단위 완료 후 PR)
  • ✅ test: 테스트 코드 추가/수정 (작업 단위 완료 후 PR)

변경 내용

  • 도메인: VO(ReleaseId, UserId), 예외 5종, 상수(WaitingConstants)
  • 애플리케이션: UseCase 4종 (대기열 등록/이탈/상태조회/구매토큰검증), Port 5종 (Redis/Kafka/gRPC), Service
  • 어댑터: REST Controller (Command/Query), gRPC 서버 (구매토큰 검증), gRPC 클라이언트 (release 판매 상태 조회), Redis 어댑터 3종 (Waiting/Admission/PurchaseToken), Kafka Producer (구매토큰 이벤트), Kafka Consumer (SSE 이벤트), 스케줄러 (입장 처리), Security
  • 설정: application-local/dev.yml, build.gradle, Dockerfile, proto (release/waiting)
  • 기술: Spring WebFlux 리액티브 스택

변경 이유

  • 릴리즈 판매 시 대기열 관리와 순차 입장, 구매 토큰 발급이 필요하며 Redis 기반 실시간 처리가 요구됨

✅ 테스트 체크리스트

  • 단위 테스트 작성 및 통과
  • 통합 테스트 통과
  • 기존 기능 정상 동작 확인 (Regression)
  • API 응답값 확인
  • 예외 케이스 처리 확인
  • 로컬 환경에서 직접 테스트 완료

📸 스크린샷 / 로그

펼쳐보기

curl-test.sh 기반 웨이팅 API 테스트 완료


🔄 동작 플로우 (Mermaid)

flowchart TD
    A[대기열 등록 요청] --> B[WaitingCommandController]
    B --> C[EnqueueService]
    C --> D[gRPC → release 판매 상태 확인]
    D -->|NOT_ON_SALE| E[ReleaseNotOnSaleException]
    D -->|ON_SALE| F[Redis SortedSet 대기열 등록]
    F --> G[AdmissionScheduler 주기적 실행]
    G --> H[입장 큐로 이동]
    H --> I[구매 토큰 발급 Redis String]
    I --> J[Kafka → SSE-Gateway 알림]
    J --> K[사용자 구매 페이지 진입]
    K --> L[gRPC → order 서비스 토큰 검증]
Loading

💬 리뷰어에게

  • Redis Sorted Set(대기열), List(입장 큐), String(구매 토큰) 3종 자료구조 활용을 검토해주세요.
  • ActiveReleaseRegistry가 판매 중인 릴리즈를 메모리에 캐싱합니다. 멀티 인스턴스 환경에서의 동기화 전략을 확인해주세요.
  • WebFlux 리액티브 스택 사용으로 기존 서비스(Spring MVC)와 다른 구조입니다.

lian2945 and others added 12 commits May 18, 2026 17:07
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 08:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

lian2945 and others added 15 commits May 18, 2026 18:04
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, grpc 설정 추가)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lian2945 and others added 29 commits May 22, 2026 12:03
CONFIRMED 상태에서 재확정을 도메인 레벨에서 차단하여 이벤트 중복 발행 방지.
서비스 레이어의 previousStatus 가드는 불필요해져 제거.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
기본값과 동일하나 운영 시 DLT 전략을 명확히 하기 위해 명시적으로 추가.
- dltStrategy = FAIL_ON_ERROR: DLT 전송 실패 시 컨슈머 중단(메시지 유실 방지)
- dltTopicSuffix = "-dlt": DLT 토픽명 명시

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java
…에러 수정

pjp.proceed()가 throws Throwable로 선언되어 있어 catch(Exception)으로는
컴파일러가 Throwable 처리를 인정하지 않아 컴파일 에러 발생.
마지막 catch 블록을 Throwable로 변경하여 수정.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java
#	services/order/src/main/java/kr/magicbox/order/adapter/in/kafka/DeliveryEventKafkaListener.java
#	services/order/src/main/java/kr/magicbox/order/adapter/in/kafka/OrderStateKafkaListener.java
#	services/order/src/main/java/kr/magicbox/order/adapter/in/kafka/PaymentEventKafkaListener.java
#	services/order/src/main/java/kr/magicbox/order/adapter/in/kafka/StockEventKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java
# Conflicts:
#	services/auth/src/main/java/kr/magicbox/auth/adapter/in/kafka/UserEventKafkaListener.java
#	services/auth/src/main/resources/application-dev.yml
#	services/auth/src/main/resources/application-prod.yml
#	services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java
#	services/creator/src/main/resources/application-prod.yml
#	services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/CreatorEventKafkaListener.java
#	services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/aop/IdempotentAspect.java
#	services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java
#	services/general-goods/src/main/resources/application-prod.yml
#	services/order/build.gradle
#	services/order/src/main/resources/application-local.yml
#	services/release/build.gradle
#	services/release/src/main/resources/application-local.yml
#	services/shopping-cart/Dockerfile
#	services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/CreatorEventKafkaListener.java
#	services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/UserEventKafkaListener.java
#	services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/aop/IdempotentAspect.java
#	services/subscribe/src/main/resources/application-prod.yml
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/out/communication/grpc/ReviewQueryGrpcAdapter.java
#	services/user/src/main/resources/application-dev.yml
#	services/user/src/main/resources/application-prod.yml
# Conflicts:
#	services/auth/src/main/java/kr/magicbox/auth/adapter/in/kafka/UserEventKafkaListener.java
#	services/auth/src/main/resources/application-dev.yml
#	services/auth/src/main/resources/application-prod.yml
#	services/creator/src/main/java/kr/magicbox/creator/adapter/in/kafka/UserEventKafkaListener.java
#	services/creator/src/main/resources/application-prod.yml
#	services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/CreatorEventKafkaListener.java
#	services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/in/kafka/aop/IdempotentAspect.java
#	services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java
#	services/general-goods/src/main/resources/application-prod.yml
#	services/order/build.gradle
#	services/order/src/main/resources/application-local.yml
#	services/shopping-cart/Dockerfile
#	services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/CreatorEventKafkaListener.java
#	services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/UserEventKafkaListener.java
#	services/subscribe/src/main/java/kr/magicbox/subscribe/adapter/in/kafka/aop/IdempotentAspect.java
#	services/subscribe/src/main/resources/application-prod.yml
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/AuthEventKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseConnectedKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/in/kafka/SseDisconnectedKafkaListener.java
#	services/user/src/main/java/kr/magicbox/user/adapter/out/communication/grpc/ReviewQueryGrpcAdapter.java
#	services/user/src/main/resources/application-dev.yml
#	services/user/src/main/resources/application-prod.yml
- release.proto: ReleaseStatus enum 추가, Release 메시지에 scheduled_at/status/limited_quantity/sold_quantity 필드 추가
- ReleaseGrpcService: toProtoRelease()에서 createdAt을 scheduledAt으로 잘못 매핑하던 버그 수정, 누락 필드 매핑 추가
- creator/release.proto: release 서비스 proto와 동기화 (ReleaseStatus enum, 누락 필드 추가)
- ReleaseResult: soldQuantity/status/scheduledAt/createdAt 필드 추가
- ReleaseStatus: 신규 enum 추가 (SCHEDULED/ON_SALE/SOLD_OUT/ENDED)
- ReleaseQueryGrpcAdapter: 신규 필드 매핑 추가, import 정렬

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- StartSaleScheduler: fixedDelay=60000 → cron="0 */10 * * * *" (10분 주기, ScheduledAtMultipleOfTenMinutes와 정렬)
- StartSaleScheduler: RedissonClient.tryLock()으로 분산 락 적용 — 락 획득 실패 시 즉시 반환
- AutoStartSaleService: 단일 트랜잭션 전체 처리 → 청크 루프(100건 단위) + AutoStartSaleChunkService에서 건별 트랜잭션
- ReleaseRepositoryPort/ReleaseJpaAdapter/ReleaseJpaRepository: findScheduledBefore에 limit 파라미터 추가 (Pageable 기반)
- build.gradle: redisson-spring-boot-starter 의존성 추가
- application-*.yml: Redis 설정 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…로로 통일

sold_quantity 증가는 Outbox→Inbox(Kafka) 경로만 사용하므로 gRPC 중복 경로 제거.
- release.proto: rpc IncreaseSoldQuantity 및 관련 메시지 제거
- ReleaseGrpcService: increaseSoldQuantity 메서드 및 IncreaseSoldQuantityUseCase 의존성 제거
- IncreaseSoldQuantityUseCase, IncreaseSoldQuantityService 삭제

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
creatorIdQueryPort.getCreatorId() (gRPC + CircuitBreaker)가 @transactional 경계 안에 포함되어
DB 커넥션을 불필요하게 점유하는 문제 수정.

- RegisterReleaseService: @transactional 제거, gRPC 조회 후 RegisterReleaseTxService에 위임
- RegisterReleaseTxService: DB 저장만 담당하는 별도 서비스로 @transactional 격리
  (self-invocation으로 AOP 프록시가 무시되는 문제를 피하기 위해 별도 클래스로 분리)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ReleaseResult: isOnSale() 파생 메서드 추가 — 판매 중 판단 로직을 DTO에 집중
- ReleaseGrpcService: isReleaseOnSale에서 enum 직접 비교 → result.isOnSale() 위임, 불필요한 ReleaseStatus import 제거
- ScheduledAtMultipleOfTenMinutesValidator: atZone() 중복 호출 제거(ZonedDateTime 한 번만), 현재 시각+10분 이후만 허용하는 최소 예약 시간 검증 추가
- ReleaseQueryGrpcAdapter: 매 호출마다 새 채널 생성 → GrpcConfiguration의 releaseManagedChannel Bean 주입으로 채널 재사용

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 주입된 creatorManagedChannel 사용

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ng 교체 (Maven Central 미존재)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
9 Security Hotspots

See analysis details on SonarQube Cloud

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.

2 participants