Skip to content

feat/116 :: Order 주문 서비스 구현#24

Open
lian2945 wants to merge 37 commits into
mainfrom
feat/116
Open

feat/116 :: Order 주문 서비스 구현#24
lian2945 wants to merge 37 commits into
mainfrom
feat/116

Conversation

@lian2945

Copy link
Copy Markdown
Collaborator

📌 관련 이슈

  • close #116

📝 변경 사항 요약

작업 유형

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

변경 내용

  • 도메인: Order/OrderLine Aggregate, OrderStatus/OrderLineDeliveryStatus Enum, 도메인 이벤트 7종, VO(OrderId, OrderLineId, ShippingAddress, UserId), 예외 7종
  • 애플리케이션: UseCase 19종 (생성/취소/조회/구매확정/자동확정/Kafka 핸들러), Port (Outbox/Repository/gRPC), Service 19종, DTO (command/query/result)
  • 어댑터: REST Controller (Command/Query), Kafka Listener 5종 (@RetryableTopic), gRPC 클라이언트 (release/waiting), JPA Persistence (Inbox/Outbox), Security, 스케줄러 (자동 구매확정)
  • 설정: application-local/dev/prod.yml, build.gradle, Dockerfile, proto (release/waiting)

변경 이유

  • 주문 생성→결제→배송→구매확정 전체 플로우를 지원하는 주문 서비스가 필요하며, Kafka 이벤트 기반으로 Orchestrator/Payment/Delivery 서비스와 연동

✅ 테스트 체크리스트

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

📸 스크린샷 / 로그

펼쳐보기

curl-test.sh 기반 주문 API 테스트 완료


🔄 동작 플로우 (Mermaid)

flowchart TD
    A[주문 생성 요청] --> B[OrderCommandController]
    B --> C[CreateOrderService]
    C --> D[Order Aggregate 생성]
    D --> E[OrderOutbox 이벤트 저장]
    E --> F[Debezium CDC → Kafka]
    F --> G[Orchestrator 수신]
    G --> H[결제 승인 커맨드]
    H --> I{결제 결과}
    I -->|성공| J[재고 차감 → 주문 확정]
    I -->|실패| K[주문 취소]
    J --> L[배송 시작 → 배송 완료]
    L --> M[자동 구매확정 스케줄러]
Loading

💬 리뷰어에게

  • Order Aggregate의 상태 전이 로직(confirm, cancel, startDelivery 등)이 도메인 불변식을 올바르게 검증하는지 확인 부탁드립니다.
  • gRPC 클라이언트(release, waiting)의 CircuitBreaker 설정과 fallback 처리를 검토해주세요.
  • AutoConfirmOrderScheduler의 자동 구매확정 조건(배송 완료 후 N일)이 적절한지 확인 필요합니다.

lian2945 and others added 4 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>
Copilot AI review requested due to automatic review settings May 18, 2026 08:19

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.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@f-lab-ted f-lab-ted 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.

검토 요구사항인 (1) Order Aggregate 상태 전이 불변식, (2) gRPC CircuitBreaker, (3) AutoConfirm 임계값을 중점적으로 살폈습니다.

잘한 점

  • 헥사고널 경계가 명확하고, Aggregate가 상태 전이마다 validateStatus/예외로 불변식을 잘 지키고 있습니다.
  • Outbox/Inbox 패턴, OrderInboxEntity.eventId unique constraint 등 분산 일관성 장치들이 정석대로 적용되었습니다.

보완할 점 (Critical/Major)

  1. Dockerfile 빌드 불가useradd ... appuserWORKDIR /appUSER appuserEXPOSE 8080이 개행 없이 붙어 있어 이미지 빌드가 실패합니다.
  2. inbox.max-event-age-minutes: 5가 너무 짧음@RetryableTopic의 최대 backoff(prod 10s × 5회) + Kafka consumer lag을 고려하면 정상 retry 메시지조차 5분 임계로 폐기될 수 있어 데이터 유실 위험.
  3. CircuitBreaker 설정 누락 + gRPC deadline 미지정 — application.yml에 resilience4j.circuitbreaker.instances.{releaseService,waitingService} 설정이 없고 blocking stub에 withDeadlineAfter가 없어, 원격 서비스가 hang되면 CB가 동작하지 않고 트랜잭션이 무한정 DB 락을 잡습니다.
  4. 트랜잭션 내 원격 호출CreateOrderService/CreateReleaseOrderService에서 @Transactional 안에서 gRPC를 호출하여 분산 락/롤백 정합성이 어긋날 위험.
  5. AutoConfirmOrderService 단일 트랜잭션 일괄 처리 — 대량 데이터를 한 트랜잭션에서 처리해 타임아웃·OOM 가능, 페이징/배치 처리 필요.
  6. OrderOutboxAdapter의 잘못된 Jackson 패키지(tools.jackson.databind.ObjectMapper) — 다른 모든 파일은 com.fasterxml.jackson. 의도된 변경이 아니면 런타임 빈 미등록 또는 NoClassDefFound 위험.

결론

Request Changes — Dockerfile 빌드 실패, Inbox 임계값 5분, CircuitBreaker 무설정 + gRPC deadline 누락, 트랜잭션 내 동기 RPC, Jackson 패키지 오류는 운영 영향이 직접적이므로 반드시 수정 바랍니다. 나머지 코멘트는 머지 전 함께 반영해 주시고, 단위 테스트 코드도 후속 PR에서 보완 부탁드립니다.

Comment thread services/order/Dockerfile Outdated
ARG JAR_FILE=build/libs/*.jar
WORKDIR /app
ARG JAR_FILE=services/order/build/libs/*.jar
RUN groupadd -r appuser && useradd -r -g appuser appuserWORKDIR /app

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Critical] 라인 3, 6에 개행이 누락되어 Dockerfile이 빌드 실패합니다.

  • 현재 RUN groupadd -r appuser && useradd -r -g appuser appuserWORKDIR /appappuserWORKDIR라는 사용자명이 만들어지고 /app 인자가 useradd에 전달되어 실패하며, WORKDIR 지시자가 인식되지 않습니다.
  • USER appuserEXPOSE 8080 또한 동일 문제로 appuserEXPOSE라는 잘못된 유저 지정.

수정안:

FROM mcr.microsoft.com/openjdk/jdk:21-ubuntu
ARG JAR_FILE=services/order/build/libs/*.jar
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY ${JAR_FILE} app.jar
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

빌드/배포 차단 이슈이므로 머지 전 반드시 수정 필요합니다.

ips: ${TRUSTED_IPS}

inbox:
max-event-age-minutes: 5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Critical] inbox.max-event-age-minutes: 5는 메시지 유실 위험이 큽니다.

IdempotentAspect#isTooOld는 임계값을 초과한 이벤트를 조용히 폐기(return null)합니다. 그런데 prod 설정상 @RetryableTopic 재시도는 최대 attempts: 5, max-delay: 10s + DLT 라우팅까지 합치면 메시지가 5분을 넘기는 시나리오가 충분히 발생합니다(컨슈머 lag, 재기동, DB 일시 장애 등).

또한 Debezium CDC 기반의 Outbox publisher가 일시 정지된 동안 누적된 정상 이벤트들도 일괄 폐기됩니다. 데이터 정합성에 직결되므로

  • 임계값을 최소 수십 분~수 시간으로 상향
  • 또는 폐기 대신 OrderInboxStatus.DEAD_LETTERED로 영속화 후 알람
    둘 중 하나가 필요합니다.

@@ -0,0 +1,26 @@
package kr.magicbox.order.adapter.out.persistence;

import tools.jackson.databind.ObjectMapper;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Critical] Jackson 패키지 import가 잘못되었습니다.

import tools.jackson.databind.ObjectMapper;

는 Jackson 3.x(tools.jackson:jackson-databind)의 새 패키지입니다. 그러나 본 모듈의 다른 파일들(event/*.java)과 Spring Boot 3.x 기본 의존성은 모두 com.fasterxml.jackson.databind.ObjectMapper(Jackson 2.x)를 사용합니다.

빌드 환경에 따라 컴파일은 통과해도 Spring Boot가 주입하는 ObjectMapper 빈은 com.fasterxml.jackson 타입이라 런타임 빈 매칭 실패로 애플리케이션 기동이 실패하거나, 잘못 매칭될 경우 직렬화 동작이 의도와 다를 수 있습니다. com.fasterxml.jackson.databind.ObjectMapper로 수정 부탁드립니다.

또한 writeValueAsString은 checked exception(JsonProcessingException)을 던지는데 현재 try/catch 없이 호출되고 있어, 패키지 수정 후 예외 처리도 함께 보강해 주세요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

저는 Jackson 3버전 Spring Boot 4버전을 사용하여서 tools.jackson.jackson-databind로 import하였습니다.

.setReleaseId(releaseId)
.build();

ReleaseServiceGrpc.ReleaseServiceBlockingStub stub = ReleaseServiceGrpc.newBlockingStub(releaseManagedChannel);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] CircuitBreaker 설정 부재 + gRPC deadline 누락 → 장애 격리 미작동.

현재 application-{local,dev,prod}.yml 어디에도 resilience4j.circuitbreaker.instances.releaseService/waitingService 설정이 없어 Resilience4j 기본값(slidingWindowSize=100, failureRateThreshold=50%)이 적용됩니다. 트래픽이 적은 시점엔 절대 OPEN되지 않을 가능성이 큽니다.

더 큰 문제는 blocking stub이 withDeadlineAfter 없이 호출된다는 점입니다. 원격 서비스가 hang되면 RPC 호출이 무한 대기 → 예외가 발생하지 않으므로 CircuitBreaker가 실패로 카운트하지도 않습니다. 결국 호출 스레드와 DB 트랜잭션(이 호출은 @Transactional 안에서 발생)이 모두 막혀 장애가 전파됩니다.

권장사항:

ReleaseServiceGrpc.newBlockingStub(releaseManagedChannel)
    .withDeadlineAfter(2, TimeUnit.SECONDS)
    .increaseSoldQuantity(request);

그리고 application.yml에 다음과 같이 명시:

resilience4j:
  circuitbreaker:
    instances:
      releaseService:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        minimumNumberOfCalls: 10
  timelimiter:
    instances:
      releaseService:
        timeoutDuration: 2s

WaitingGrpcAdapter도 동일하게 보강 필요합니다.

@Transactional
@Override
public void createReleaseOrder(CreateReleaseOrderCommand command) {
boolean valid = purchaseTokenValidationPort.validate(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] @Transactional 범위 안에서 동기 gRPC를 호출하고 있습니다.

purchaseTokenValidationPort.validate(...) (waiting gRPC)와 releaseIncreaseSoldQuantityPort.increaseSoldQuantity(...) (release gRPC) 모두 트랜잭션 안에서 호출됩니다.

  • gRPC가 느려지면 DB 커넥션과 row lock이 그대로 점유되어 connection pool exhaustion 위험.
  • increaseSoldQuantity가 성공 후 로컬 commit이 실패하면 재고 카운터만 증가하는 데이터 불일치 발생(외부 시스템은 보상이 어려움).

권장:

  1. 토큰 검증은 트랜잭션 시작 이전에 별도 호출.
  2. 재고 증가는 Outbox 이벤트로 비동기 발행 후 release 서비스가 Kafka 컨슈머로 처리(이미 다른 흐름은 Outbox로 처리하고 있으므로 일관성 측면에서도 적절).

CreateOrderService에는 RPC 호출이 없어 괜찮지만, 같은 원칙으로 추후 추가 시 주의 필요.


@Transactional
@Override
public void autoConfirmDeliveredOrders() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] 단일 트랜잭션에서 전체 배치 처리 → 타임아웃/OOM/락 경합 위험.

  • findDeliveredBefore 결과를 모두 메모리에 로드한 뒤 한 트랜잭션에서 처리합니다. 운영 환경에서 누적 주문 수가 수만~수십만이면 트랜잭션 타임아웃과 메모리 부담이 큽니다.
  • 한 건이 실패하면 전체 롤백되어 정상 건들까지 재처리됩니다.
  • AUTO_CONFIRM_DAYS = 7이 코드 상수로 하드코딩되어 정책 변경 시 재배포가 필요합니다.

권장 개선:

  1. 페이지/청크 단위 조회 + 청크별 트랜잭션(@Transactional 메서드를 Spring Batch나 별도 트랜잭션 헬퍼로 분리).
  2. AUTO_CONFIRM_DAYS@ConfigurationProperties로 외부화.
  3. orderRepositoryPort.findDeliveredBeforePageable/limit 추가, 처리된 건은 다음 조회에서 자연스레 제외(status != DELIVERED).
  4. 스케줄러 분산 락(예: ShedLock) — 멀티 인스턴스 운영 시 중복 실행 방지.

pjp.proceed();
} catch (Throwable e) {
status.setRollbackOnly();
throw new RuntimeException(e);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] Throwable을 그대로 RuntimeException으로 감싸면 재시도 정책이 무력화됩니다.

} catch (Throwable e) {
    status.setRollbackOnly();
    throw new RuntimeException(e);
}

@RetryableTopic은 예외 타입으로 retryable/non-retryable을 구분하는데, 여기서 모든 예외가 RuntimeException으로 균일화되어 도메인 예외(InvalidPurchaseTokenException 등 재시도해도 같은 결과)도 무한 retry 후 DLT로 들어갑니다. 또한 root cause 스택트레이스만 보이고 카테고리가 사라져 운영 분석이 어렵습니다.

권장:

  • catch (RuntimeException e) { ... throw e; } + catch (Throwable e) { ... throw new IllegalStateException(e); } 식으로 RuntimeException은 그대로 전파.
  • 추가로 @RetryableTopic(include=..., exclude=...)로 비재시도 예외 명시.

또한 pjp.proceed()Throwable을 던지므로 호출부에서 Error까지 잡아 wrap하는 것은 위험합니다. Error는 다시 던지도록 분리 권장.

* 특정 OrderLine을 CONFIRMED 상태로 전환한다.
* 모든 라인이 CONFIRMED 이상이면 Order도 CONFIRMED로 전환한다.
*/
public void confirmOrderLine(Long orderLineId) {

@f-lab-ted f-lab-ted May 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] confirmOrderLine 호출 시 Outbox 이벤트 발행 시점이 불명확합니다.

현재 도메인 구조상

  • confirm()(전체 확정) → ConfirmOrderService에서 OrderConfirmedEvent 1회 발행 OK
  • confirmOrderLine(lineId)(개별 확정) → ConfirmOrderLineService에서 어떻게 발행하는지 호출 흐름이 확실치 않음.

특히 partial confirm 상태(PREPARING 유지)에서는 Order 상태 변경이 없으니 이벤트가 안 나가는 게 맞지만, 마지막 라인이 CONFIRMED 되어 Order가 CONFIRMED로 전이되는 순간에만 이벤트를 발행하도록 호출 서비스에서 분기 처리가 보장돼야 합니다. CancelOrderService처럼 if (order.getStatus() == OrderStatus.CONFIRMED && previous != CONFIRMED) 가드가 누락되면 이벤트가 누락되거나 중복 발행됩니다.

ConfirmOrderLineService 코드 확인 및 필요시 가드 추가 부탁드립니다.

protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
String clientIp = request.getRemoteAddr();

if (!trustedIpProperties.getIps().contains(clientIp)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] X-User-Id 헤더 신뢰는 IP만으로 충분하지 않습니다.

현재 흐름: getRemoteAddr()trusted.ips 이면 X-User-Id 헤더 값을 그대로 인증 주체로 사용.

문제점:

  1. ForwardedHeaderFilter가 적용돼도 remoteAddr는 결국 직전 hop(=API Gateway/LB) IP가 됩니다. trusted ip 목록이 LB의 IP면 그 LB를 거치는 누구나 임의 X-User-Id를 주입 가능합니다(서비스 메시 내부 침투 시 권한 상승 1-shot).
  2. UserId VO의 검증(Long.parseLong > 0) 외에 user 존재 확인이 없어, 존재하지 않는 userId도 통과됩니다.
  3. Long.valueOf(userIdHeader)는 이미 isValidUserId에서 검증했으나 throw가 catch되어도 NPE/숫자 오버플로 등 edge case 대비가 약함.

권장:

  • mTLS 또는 게이트웨이가 발급한 서명된 헤더(JWT/HMAC)로 신원 증명.
  • 최소한 게이트웨이가 X-User-Id와 함께 X-Auth-Signature를 발급하고 본 필터에서 서명 검증.
  • trusted IP 목록은 CIDR 기반 매칭 + 운영 환경에서 외부 노출 금지.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Istio 적용 후 AuthorizationPolicy로 대체할 예정입니다!

attempts: 3
backoff:
delay: 1s
max-delay: 3s

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Major] @RetryableTopic의 DLT 전략이 명시적이지 않습니다.

현재 spring.kafka.retry.topic.attempts만 설정되어 있고, DLT(Dead Letter Topic) 라우팅과 핸들러가 없습니다. retry 소진된 메시지가 어떤 토픽으로 가는지, 어떤 식으로 모니터링/재처리되는지 운영 시 추적이 어렵습니다.

권장:

  • @RetryableTopic(dltStrategy = DltStrategy.FAIL_ON_ERROR, dltTopicSuffix = "-dlt", ...) 명시.
  • @DltHandler 메서드로 DLT 적재 시 알람/메트릭 노출.
  • 비재시도 예외(InvalidFieldException, OrderUnauthorizedException 등) 화이트리스트로 즉시 DLT.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

현재 구현되어 있고, 명시적으로 변경하였습니다!

lian2945 and others added 21 commits May 19, 2026 17:27
…, 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>
@RetryableTopic 전환 이후 DLT 발생 시 Inbox 상태가 업데이트되지 않던
문제를 해결합니다. 각 리스너에 @DltHandler를 추가하여 재시도 소진 후
DLT로 전환될 때 해당 Inbox 레코드를 DEAD_LETTERED로 마킹합니다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AuthEventKafkaListener: @RetryableTopic, @DltHandler 추가 및 UserInboxRepository 주입
- SseConnectedKafkaListener, SseDisconnectedKafkaListener: @RetryableTopic 추가
- KafkaConfiguration: @EnableKafkaRetryTopic, ThreadPoolTaskScheduler 빈 추가

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>
…Repository 누락 추가

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xRepositoryPort 참조 수정

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>
- IdempotentAspect#isTooOld 시 return null 대신
  DEAD_LETTERED 상태로 저장하여 추적 가능하게 변경
- prod max-event-age-minutes: 5 → 60 (재시도·lag 여유 확보)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lian2945 and others added 11 commits May 20, 2026 20:12
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rvice, waitingService)

- application-dev/prod/local.yml 에 releaseService, waitingService 인스턴스 설정 명시
- COUNT_BASED slidingWindowSize=20, failureRateThreshold=50%, waitDurationInOpenState=10s
- TimeLimiter timeoutDuration=2s (gRPC withDeadlineAfter와 동일 기준)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 토큰 검증(waiting gRPC)을 @transactional 이전에 호출하도록 분리
- increaseSoldQuantity gRPC 제거 → ReleaseSoldQuantityIncreaseEvent Outbox 비동기 발행으로 교체
- ReleaseGrpcAdapter, ReleaseIncreaseSoldQuantityPort, ReleaseServiceUnavailableException 삭제
- release-service gRPC 채널 설정 및 releaseService CircuitBreaker/TimeLimiter 설정 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AUTO_CONFIRM_DAYS 하드코딩 제거 → order.auto-confirm.days/chunk-size 설정 외부화
- findDeliveredBefore에 limit(Pageable) 추가로 전체 로드 방지
- 단일 트랜잭션 전체 배치 → AutoConfirmOrderChunkService.confirmOne()으로 분리
  self-invocation 문제 해결, 한 건 실패해도 다른 건 롤백 없이 계속 처리
- Redisson 분산 락 적용(@SchedulerLock): 멀티 인스턴스 중복 실행 방지
- spring.data.redis 설정 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…clude 추가

- IdempotentAspect: catch(Throwable→RuntimeException) wrap으로 인한 retry 정책 무력화 수정
  - Error는 즉시 rethrow
  - RuntimeException은 타입 보존하여 rethrow
  - checked Exception만 IllegalStateException으로 wrap
- 전 서비스 KafkaListener: @RetryableTopic(exclude = {BusinessException.class}) 추가
  - BusinessException(4xx)은 재시도 불필요하므로 즉시 DLT로 전송

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Order.confirmOrderLine()은 CONFIRMED 상태에서도 호출 가능(partial confirm 이후 재호출)하므로
이미 CONFIRMED인 상태에서 라인 확정 시 OrderConfirmedEvent가 중복 발행되는 버그 수정.
PREPARING → CONFIRMED 전이 시에만 이벤트를 발행하도록 previousStatus 가드 추가.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
…에러 수정

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

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

Copy link
Copy Markdown

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.

3 participants