Skip to content

feat/120 :: Orchestrator 주문 사가 오케스트레이터 구현#28

Open
lian2945 wants to merge 134 commits into
mainfrom
feat/120
Open

feat/120 :: Orchestrator 주문 사가 오케스트레이터 구현#28
lian2945 wants to merge 134 commits into
mainfrom
feat/120

Conversation

@lian2945

Copy link
Copy Markdown
Collaborator

📌 관련 이슈

  • close #120

📝 변경 사항 요약

작업 유형

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

변경 내용

  • 도메인: 커맨드 이벤트 6종, OrchestratorCommandEventType Enum, 글로벌 예외 계층
  • 애플리케이션: UseCase 11종, OutboxPort, Service 11종
  • 어댑터: Kafka Listener 4종, Kafka 이벤트 DTO 11종, Inbox/Outbox Persistence, InboxProperties, KafkaConfiguration
  • 설정: application-local/dev/prod.yml, build.gradle, Dockerfile, Gradle Wrapper

변경 이유

  • 주문-결제-재고-배송-정산의 분산 트랜잭션을 중앙에서 조율하는 사가 오케스트레이터가 필요하며, 각 단계의 성공/실패에 따른 보상 트랜잭션을 관리

✅ 테스트 체크리스트

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

📸 스크린샷 / 로그

펼쳐보기

curl-test.sh 기반 사가 흐름 테스트 완료


🔄 동작 플로우 (Mermaid)

flowchart TD
    A[Order 이벤트 수신] --> B{이벤트 타입}
    B -->|order-prepare| C[재고 차감 커맨드 발행]
    B -->|order-confirmed| D[결제 승인 커맨드 발행]
    B -->|order-cancel| E[결제 취소 커맨드 발행]
    C --> F{재고 결과}
    F -->|성공| G[주문 준비 확정 커맨드]
    F -->|실패| H[주문 취소 처리]
    D --> I{결제 결과}
    I -->|성공| J[정산 준비 커맨드]
    I -->|실패| K[재고 복원 커맨드]
    L[배송 완료 이벤트] --> M[구매확정 이벤트]
    M --> N[정산 실행 커맨드]
Loading

💬 리뷰어에게

  • 사가 단계별 보상 트랜잭션 흐름이 누락 없는지 검토해주세요.
  • Inbox 패턴의 멱등성 보장이 모든 이벤트 핸들러에 적용되었는지 확인 부탁드립니다.
  • Outbox 이벤트의 event_type 필드가 Debezium EventRouter 라우팅 규칙과 일치하는지 확인 필요합니다.

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>
… Gradle Wrapper

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:36

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 17:53
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>
…rt 제거, security.trusted.ips 추가

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>
lian2945 and others added 29 commits May 22, 2026 12:08
기본값과 동일하나 운영 시 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/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-local.yml
#	services/general-goods/src/main/resources/application-prod.yml
#	services/orchestrator/.gitattributes
#	services/orchestrator/.gitignore
#	services/orchestrator/gradle/wrapper/gradle-wrapper.properties
#	services/orchestrator/gradlew
#	services/orchestrator/gradlew.bat
#	services/orchestrator/src/main/java/kr/magicbox/orchestrator/OrchestratorApplication.java
#	services/orchestrator/src/main/resources/application.yml
#	services/orchestrator/src/test/java/kr/magicbox/orchestrator/OrchestratorApplicationTests.java
#	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-local.yml
#	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
#	settings.gradle
- 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