Skip to content

[FEAT] 자재 주문 목록 이벤트 발행#2

Merged
taemin3 merged 1 commit into
mainfrom
SPM-424
Nov 6, 2025
Merged

[FEAT] 자재 주문 목록 이벤트 발행#2
taemin3 merged 1 commit into
mainfrom
SPM-424

Conversation

@taemin3
Copy link
Copy Markdown
Contributor

@taemin3 taemin3 commented Nov 6, 2025

📝 Summary

  • 자재 주문 목록 이벤트 발행

🙏 Question & PR point

📬 Reference

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 구매 주문을 수령 처리할 수 있는 새로운 기능 추가
    • 주문 항목에 리드타임 정보 추가
    • 예정 배송일자 필드 추가
  • 개선 사항

    • 필수일자 필드의 정밀도 향상 (날짜만 → 날짜 및 시간)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 6, 2025

Walkthrough

주문 수신 기능을 추가하고, 이벤트 기반 아키텍처로 전환합니다. Spring Kafka 의존성을 도입하고, 주문 생성/취소/삭제/수신 이벤트를 아웃박스 패턴으로 관리하는 새로운 인프라를 구축합니다.

Changes

코호트 / 파일(들) 변경 사항 요약
의존성 및 설정
build.gradle
Spring Kafka 및 Jackson Databind 의존성 추가
애플리케이션 설정
src/main/java/com/sampoom/purchase/PurchaseApplication.java
@EnableScheduling 애노테이션 추가로 스케줄링 지원 활성화
서버 설정 변경
src/main/java/com/sampoom/purchase/common/config/swagger/SwaggerConfig.java
로컬 서버 포트를 8080에서 8081로 변경
REST 엔드포인트 확장
src/main/java/com/sampoom/purchase/api/purchase/controller/PurchaseController.java
주문 수신 처리를 위한 PATCH /{orderId}/receive 엔드포인트 추가
요청/응답 DTO 업데이트
src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderRequestDto.java, src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderResponseDto.java, src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderItemDto.java
requiredAt 필드를 LocalDate에서 LocalDateTime으로 변경, expectedDeliveryAt 필드 추가, leadTimeDays 필드 추가
엔티티 확장
src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrder.java, src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrderItem.java
items 관계 추가, expectedDeliveryAt/leadTimeDays 필드 추가, requiredAt 타입 변경
핵심 비즈니스 로직
src/main/java/com/sampoom/purchase/api/purchase/service/PurchaseService.java
receiveOrder 메서드 추가, PurchaseEventService 통합, expectedDeliveryAt 계산 로직 추가, 모든 주문 작업에서 이벤트 발행
이벤트 인프라
src/main/java/com/sampoom/purchase/common/event/OutboxStatus.java, src/main/java/com/sampoom/purchase/common/event/PurchaseEvent.java, src/main/java/com/sampoom/purchase/common/event/PurchaseEventService.java
이벤트 상태 열거형, 이벤트 페이로드 레코드, 이벤트 기록 서비스 신규 추가
아웃박스 패턴 구현
src/main/java/com/sampoom/purchase/common/event/PurchaseOutbox.java, src/main/java/com/sampoom/purchase/common/event/PurchaseOutboxRepository.java, src/main/java/com/sampoom/purchase/common/event/PurchaseOutboxPublisher.java
아웃박스 엔티티, 레포지토리, Kafka 퍼블리셔 신규 추가 (재시도 로직 및 지수 백오프 포함)

Sequence Diagram(s)

sequenceDiagram
    participant Client as 클라이언트
    participant Controller as PurchaseController
    participant Service as PurchaseService
    participant EventService as PurchaseEventService
    participant OutboxRepo as PurchaseOutboxRepository
    participant Publisher as PurchaseOutboxPublisher
    participant Kafka as Kafka
    
    Client->>Controller: PATCH /{orderId}/receive
    activate Controller
    Controller->>Service: receiveOrder(orderId)
    activate Service
    
    Service->>Service: 주문 조회 및 상태 변경
    Service->>Service: 주문 아이템 로드
    Service->>EventService: recordOrderReceived(order)
    activate EventService
    
    EventService->>EventService: PurchaseEvent 생성
    EventService->>EventService: ObjectMapper로 직렬화
    EventService->>OutboxRepo: PurchaseOutbox.ready 저장
    deactivate EventService
    
    Service->>Controller: ResponseEntity<ApiResponse> 반환
    deactivate Service
    Controller->>Client: 200 OK
    deactivate Controller
    
    Note over Publisher: 스케줄된 작업 (Fixed Delay)
    Publisher->>OutboxRepo: pickReadyBatch(batchSize, maxRetry)
    OutboxRepo-->>Publisher: READY/FAILED 아웃박스 항목
    
    loop 각 아웃박스 항목 처리
        Publisher->>Kafka: KafkaTemplate.send(topic, payload)
        alt 발행 성공
            Publisher->>OutboxRepo: markPublished()
        else 발행 실패
            Publisher->>Publisher: computeBackoffMs(retryCount)
            Publisher->>OutboxRepo: markFailed(error, nextRetryAt)
            alt 재시도 초과
                Publisher->>OutboxRepo: markDead(error)
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (복잡) | ⏱️ ~50분

특히 주의가 필요한 영역:

  • PurchaseOutboxPublisher.java: 지수 백오프 및 재시도 로직의 정확성, Kafka 퍼블리싱 실패 처리 및 상태 전이 흐름 검증
  • PurchaseService.java: 이벤트 서비스 통합 시 기존 비즈니스 로직과의 일관성, 주문 항목 로드 타이밍, expectedDeliveryAt 계산 로직의 정확성
  • PurchaseEventService.java: 이벤트 페이로드 구성, JSON 직렬화 오류 처리, 주문 항목 변환(Long → int) 시 데이터 손실 가능성
  • 엔티티 관계: PurchaseOrder와 PurchaseOrderItem 간 양방향 관계 설정, cascade 설정의 영향 범위
  • DTO 변경사항: LocalDate → LocalDateTime 변경의 API 호출자에 미치는 영향, 기존 데이터 마이그레이션 전략

Possibly related PRs

  • [FEAT] 구매 관리 api 구현 #1: 동일한 purchase-domain 클래스들을 수정하며, 주문 수신 엔드포인트, 스케줄링, 아웃박스/이벤트 타입 및 퍼블리셔, DTO/엔티티/서비스 확장을 추가합니다.

Suggested reviewers

  • yangjiseonn
  • vivivim
  • CHOOSLA

Poem

🐰 주문 오는 그 날을 위해,
아웃박스에 담아 안전하게,
Kafka의 흐름을 타고 가는 이벤트,
재시도와 백오프의 춤을 추며,
신뢰할 수 있는 배송이 되어라! 📦✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 자재 주문 목록 이벤트 발행을 다루며, 변경사항의 핵심(이벤트 발행 기능 추가)을 반영합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch SPM-424

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
src/main/java/com/sampoom/purchase/common/config/swagger/SwaggerConfig.java (1)

14-71: 주석 처리된 코드 제거 권장

JWT 인증 관련 주석 처리된 설정 코드가 58줄에 걸쳐 남아있습니다. 버전 관리 시스템(Git)이 있으므로 필요시 이력에서 복원 가능합니다. 코드베이스의 가독성을 위해 제거를 권장합니다.

-//    @Value("${jwt.access.header}")
-//    private String accessTokenHeader;
-//
-//    @Value("${jwt.refresh.header}")
-//    private String refreshTokenHeader;
-
     @Bean
     public OpenAPI openAPI() {
         Server localServer = new Server()
                 .url("http://localhost:8081/")
                 .description("로컬 서버");

         Server prodServer = new Server()
                 .url("https://sampoom.store/api/purchase")
                 .description("배포 서버");

         return new OpenAPI()
                 .info(new Info()
                         .title("삼삼오토 Purchase Service API")
                         .description("Purchase 서비스 REST API 문서")
                         .version("1.0.0"))
                 .servers(List.of( prodServer,localServer));
     }
-
-//    @Bean
-//    public OpenAPI openAPI() {
-//        SecurityScheme accessTokenScheme = new SecurityScheme()
-//                .type(SecurityScheme.Type.APIKEY)   // 여기 중요!
-//                .in(SecurityScheme.In.HEADER)
-//                .name(accessTokenHeader); // 일반적으로 "Authorization"
-//
-//        SecurityRequirement accessTokenRequirement = new SecurityRequirement()
-//                .addList(accessTokenHeader);
-//
-//        SecurityScheme refreshTokenScheme = new SecurityScheme()
-//                .type(SecurityScheme.Type.APIKEY)
-//                .in(SecurityScheme.In.HEADER)
-//                .name(refreshTokenHeader); // 예: "Refresh"
-//
-//        SecurityRequirement refreshTokenRequirement = new SecurityRequirement()
-//                .addList(refreshTokenHeader);
-//
-//        Server server = new Server();
-//        server.setUrl("http://localhost:8080");
-//
-//
-//        return new OpenAPI()
-//                .info(new Info()
-//                        .title("삼삼오토")
-//                        .description("삼삼오토 REST API Document")
-//                        .version("1.0.0"))
-//                .components(new Components()
-//                        .addSecuritySchemes(accessTokenHeader, accessTokenScheme)
-//                        .addSecuritySchemes(refreshTokenHeader, refreshTokenScheme))
-//                .addServersItem(server)
-//                .addSecurityItem(accessTokenRequirement)
-//                .addSecurityItem(refreshTokenRequirement);
-//    }
 }
src/main/java/com/sampoom/purchase/common/event/OutboxStatus.java (1)

1-8: Outbox 상태 관리 Enum 추가

아웃박스 패턴의 메시지 상태를 관리하는 enum이 적절하게 정의되었습니다. 각 상태의 의미가 명확합니다.

선택적으로, 각 상태 전환 조건과 의미를 JavaDoc으로 문서화하면 유지보수성이 향상됩니다:

 package com.sampoom.purchase.common.event;

+/**
+ * 아웃박스 메시지의 처리 상태를 나타냅니다.
+ * <ul>
+ *   <li>READY: 발행 대기 중</li>
+ *   <li>PUBLISHED: 발행 완료</li>
+ *   <li>FAILED: 발행 실패 (재시도 예정)</li>
+ *   <li>DEAD: 최대 재시도 횟수 초과로 처리 불가</li>
+ * </ul>
+ */
 public enum OutboxStatus {
     READY,
     PUBLISHED,
     FAILED,
     DEAD
 }
src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrderItem.java (1)

35-35: 리드타임 필드 추가

자재별 리드타임을 추가하는 변경사항입니다. Integer 타입을 사용하여 null 값을 허용하고 있습니다.

다음 사항을 고려해주세요:

  1. Null 처리: leadTimeDays가 null일 때의 동작을 명확히 정의하고, 비즈니스 로직에서 적절히 처리되는지 확인이 필요합니다.
  2. 유효성 검증: 음수 값 방지를 위해 @Min(0) 또는 @PositiveOrZero 제약 조건 추가를 고려하세요.
+import jakarta.validation.constraints.PositiveOrZero;
+
 @Entity
 @Getter
 @Table(name = "purchase_order_item")
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @AllArgsConstructor
 @Builder
 public class PurchaseOrderItem {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     @Column(name = "purchase_order_item_id")
     private Long id;

     private Long quantity;

     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "purchase_order_id")
     private PurchaseOrder purchaseOrder;

     private String materialCode;

     private String materialName;

     private String unit;

     @Column(precision = 19, scale = 2)
     private BigDecimal unitPrice;

+    @PositiveOrZero
     private Integer leadTimeDays; // 자재 리드타임 (일 단위)
 }
src/main/java/com/sampoom/purchase/common/event/PurchaseOutbox.java (2)

12-61: 배치 조회를 위한 인덱스 추가를 고려하세요.

PurchaseOutboxPublisherstatusnextRetryAt 컬럼을 사용하여 발행 가능한 outbox 엔트리를 주기적으로 조회할 것으로 예상됩니다. 이러한 컬럼에 복합 인덱스를 추가하면 배치 조회 성능이 크게 향상됩니다.

다음과 같이 인덱스를 추가하는 것을 권장합니다:

 @Table(
         name = "purchase_outbox",
         uniqueConstraints = {
                 @UniqueConstraint(name = "uq_purchase_outbox_event_id", columnNames = {"event_id"})
-        }
+        },
+        indexes = {
+                @Index(name = "idx_purchase_outbox_status_retry", columnList = "status, nextRetryAt")
+        }
 )

85-95: 파라미터 유효성 검사를 추가하는 것을 고려하세요.

ready() 팩토리 메서드가 null 파라미터에 대한 검증을 수행하지 않습니다. 데이터베이스 제약조건에서 일부는 잡히겠지만, 명시적인 검증을 통해 더 명확한 오류 메시지를 제공할 수 있습니다.

 public static PurchaseOutbox ready(Long aggregateId, String eventType, UUID eventId, JsonNode payloadJson) {
+    if (aggregateId == null || eventType == null || eventId == null || payloadJson == null) {
+        throw new IllegalArgumentException("Required parameters cannot be null");
+    }
     return PurchaseOutbox.builder()
             .aggregateId(aggregateId)
             .eventType(eventType)
             .eventId(eventId)
             .payload(payloadJson)
             .occurredAt(LocalDateTime.now())
             .status(OutboxStatus.READY)
             .retryCount(0)
             .build();
 }
src/main/java/com/sampoom/purchase/common/event/PurchaseEvent.java (1)

23-28: Material의 quantity 타입 선택을 확인하세요.

Material 레코드의 quantity 필드가 Integer 타입을 사용하는 반면, 도메인 엔티티 PurchaseOrderItemLong 타입을 사용할 가능성이 높습니다.

Integer의 최대값은 약 21억으로 일반적인 구매 주문에는 충분하지만, 향후 대량 주문이나 집계 시나리오를 고려한다면 타입 불일치가 문제가 될 수 있습니다. 이벤트 페이로드에서 Integer 사용이 의도적인 설계 결정인지 확인하시기 바랍니다.

만약 일관성을 위해 Long을 사용하려면:

 public record Material(
         String materialCode,
         String materialName,
-        Integer quantity,
+        Long quantity,
         String unit
 ) {}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e79bf42 and 06b75b6.

📒 Files selected for processing (16)
  • build.gradle (1 hunks)
  • src/main/java/com/sampoom/purchase/PurchaseApplication.java (1 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/controller/PurchaseController.java (1 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderItemDto.java (2 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderRequestDto.java (2 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderResponseDto.java (2 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrder.java (3 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrderItem.java (1 hunks)
  • src/main/java/com/sampoom/purchase/api/purchase/service/PurchaseService.java (4 hunks)
  • src/main/java/com/sampoom/purchase/common/config/swagger/SwaggerConfig.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/event/OutboxStatus.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/event/PurchaseEvent.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/event/PurchaseEventService.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/event/PurchaseOutbox.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/event/PurchaseOutboxPublisher.java (1 hunks)
  • src/main/java/com/sampoom/purchase/common/event/PurchaseOutboxRepository.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/sampoom/purchase/common/event/PurchaseEventService.java (1)
src/main/java/com/sampoom/purchase/api/purchase/service/PurchaseService.java (1)
  • Service (28-275)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (12)
src/main/java/com/sampoom/purchase/PurchaseApplication.java (1)

5-8: 스케줄링 활성화

아웃박스 메시지의 주기적 발행을 위해 @EnableScheduling 어노테이션이 추가되었습니다. 이는 PurchaseOutboxPublisher의 배치 발행 작업에 필요한 설정입니다.

src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderResponseDto.java (2)

27-29: 날짜 필드 타입 변경 및 신규 필드 추가

requiredAtLocalDate에서 LocalDateTime으로 변경되고, expectedDeliveryAt 필드가 추가되었습니다.

Breaking Change 주의: requiredAt의 타입 변경은 API 응답 형식을 변경하는 breaking change입니다:

  • 변경 전: "requiredAt": "2025-11-06"
  • 변경 후: "requiredAt": "2025-11-06T12:00:00"

다음 사항을 확인해주세요:

  1. API 소비자(프론트엔드, 다른 서비스)가 이 변경을 처리할 수 있는지 확인
  2. API 버전 관리 또는 마이그레이션 계획이 있는지 확인
  3. 날짜/시간 직렬화 형식을 명시적으로 설정하는 것을 권장합니다 (예: @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss"))

46-46: 예상 배송일 매핑 추가

expectedDeliveryAt 필드가 빌더에 적절히 매핑되었습니다.

src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderItemDto.java (2)

21-21: 리드타임 필드 추가

엔티티 변경사항과 일치하는 DTO 필드가 추가되었습니다.


30-30: 리드타임 매핑 추가

from() 메서드에서 leadTimeDays 필드가 적절히 매핑되었습니다.

src/main/java/com/sampoom/purchase/common/config/swagger/SwaggerConfig.java (1)

23-23: 검토 의견이 기반한 오류 발견

SwaggerConfig의 .url("http://localhost:8081/")은 Swagger API 문서의 설정 포인터일 뿐, 실제 애플리케이션 서버 포트와는 무관합니다. Spring Boot의 서버 포트는 server.port 속성을 통해 application.properties나 application.yml 파일에서 설정됩니다. 이 두 설정은 독립적이므로 Swagger 문서화 URL의 포트 변경이 애플리케이션 설정과 일치할 필요가 없습니다.

검토 의견이 기반한 오류 발견

SwaggerConfig의 .url("http://localhost:8081/")은 Swagger API 문서의 설정 포인터일 뿐, 실제 애플리케이션 서버 포트와는 무관합니다. Spring Boot의 서버 포트는 server.port 속성을 통해 application.properties나 application.yml 파일에서 설정됩니다. 이 두 설정은 독립적이므로 Swagger 문서화 URL의 포트 변경이 애플리케이션 설정과 일치할 필요가 없습니다.

src/main/java/com/sampoom/purchase/api/purchase/dto/PurchaseOrderRequestDto.java (1)

21-22: LocalDateTime 전환을 확인했습니다

요청 시각에 시간 정보까지 담을 수 있게 되어 요구사항에 부합해 보입니다.

src/main/java/com/sampoom/purchase/api/purchase/entity/PurchaseOrder.java (1)

34-56: 엔티티 확장 내용 확인 완료

필드 확장과 아이템 연관관계 설정이 도메인 모델 요구에 부합해 보입니다.

src/main/java/com/sampoom/purchase/api/purchase/service/PurchaseService.java (4)

48-56: 리드타임 계산 및 예상 배송일 설정이 잘 구현되었습니다.

자재들 중 최대 리드타임을 계산하고 이를 기반으로 예상 배송일을 설정하는 로직이 정확합니다.


73-74: 람다에서 사용하기 위한 final 변수 복사가 적절합니다.

람다 표현식 내에서 사용하기 위해 effectively final 변수를 생성한 것은 올바른 접근입니다.


220-252: 주문 수신 기능이 잘 구현되었습니다.

새로운 receiveOrder() 메서드가 주문 상태를 업데이트하고 이벤트를 발행하는 로직이 올바르게 구현되어 있습니다. 트랜잭션 처리와 이벤트 발행이 적절히 조율되어 있습니다.


254-274: 주문 코드 형식 변경을 확인해 주세요.

주문 코드의 날짜 부분이 전체 날짜에서 연도만 사용하도록 변경되었습니다 (예: ORD-20251106-001ORD-2025-001). 이는 다음과 같은 영향을 미칩니다:

  • 시퀀스가 일별이 아닌 연별로 리셋됩니다
  • 연간 주문량이 많을 경우 시퀀스 번호가 더 길어집니다
  • 주문 코드에서 날짜 정보의 세분성이 낮아집니다

이것이 의도된 비즈니스 요구사항인지 확인해 주세요.

Comment thread build.gradle
Copy link
Copy Markdown

@yangjiseonn yangjiseonn left a comment

Choose a reason for hiding this comment

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

확인했습니다

@taemin3 taemin3 merged commit 1e325af into main Nov 6, 2025
8 checks passed
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