Skip to content

feat/119 :: Delivery 배송 서비스 구현#27

Open
lian2945 wants to merge 125 commits into
mainfrom
feat/119
Open

feat/119 :: Delivery 배송 서비스 구현#27
lian2945 wants to merge 125 commits into
mainfrom
feat/119

Conversation

@lian2945

Copy link
Copy Markdown
Collaborator

📌 관련 이슈

  • close #119

📝 변경 사항 요약

작업 유형

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

변경 내용

  • 도메인: Delivery/TrackingHistory Aggregate, DeliveryStatus/TrackingHistoryStatus Enum, 도메인 이벤트 2종, VO 4종, 예외 3종
  • 애플리케이션: UseCase 2종, Port 3종, Service 4종, DTO
  • 어댑터: REST Controller, SweetTracker Feign Client 연동, JPA Persistence, Outbox, 배송 추적 스케줄러, Security
  • 설정: application-local/dev/prod.yml, build.gradle, Dockerfile, Gradle Wrapper

변경 이유

  • 주문 확정 후 배송 시작부터 배송 완료까지의 추적 흐름을 관리하는 독립 서비스가 필요하며, 외부 택배사 API 연동을 통해 실시간 배송 상태를 동기화

✅ 테스트 체크리스트

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

📸 스크린샷 / 로그

펼쳐보기

curl-test.sh 기반 배송 API 테스트 완료


🔄 동작 플로우 (Mermaid)

flowchart TD
    A[배송 시작 요청] --> B[DeliveryCommandController]
    B --> C[StartDeliveryService]
    C --> D[Delivery Aggregate 생성]
    D --> E[DeliveryOutbox 이벤트 저장]
    E --> F[Debezium CDC - Kafka 발행]
    F --> G[Order/Orchestrator 수신]
    H[DeliveryTrackingScheduler 주기 실행] --> I[SweetTracker API 조회]
    I --> J{배송 완료 여부}
    J -->|미완료| K[TrackingHistory 업데이트]
    J -->|완료| L[DeliveryStatus DELIVERED]
    L --> M[DeliveryCompleted 이벤트 발행]
Loading

💬 리뷰어에게

  • SweetTracker Feign Client의 에러 디코더와 타임아웃 설정을 검토해주세요.
  • DeliveryTrackingScheduler의 폴링 주기가 적절한지 확인 필요합니다.
  • Outbox 이벤트 발행 시 delivery-started/delivery-completed 토픽 네이밍을 검토해주세요.

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>
…rity)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dle 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:35

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:43
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>
…RUSTED_IPS 제거, openfeign/resilience4j 추가)

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
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
#	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>
# Conflicts:
#	services/auth/src/main/resources/application-dev.yml
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