Conversation
Walkthrough주문 수신 기능을 추가하고, 이벤트 기반 아키텍처로 전환합니다. Spring Kafka 의존성을 도입하고, 주문 생성/취소/삭제/수신 이벤트를 아웃박스 패턴으로 관리하는 새로운 인프라를 구축합니다. Changes
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
Estimated code review effort🎯 4 (복잡) | ⏱️ ~50분 특히 주의가 필요한 영역:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 값을 허용하고 있습니다.다음 사항을 고려해주세요:
- Null 처리:
leadTimeDays가 null일 때의 동작을 명확히 정의하고, 비즈니스 로직에서 적절히 처리되는지 확인이 필요합니다.- 유효성 검증: 음수 값 방지를 위해
@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: 배치 조회를 위한 인덱스 추가를 고려하세요.
PurchaseOutboxPublisher가status와nextRetryAt컬럼을 사용하여 발행 가능한 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타입을 사용하는 반면, 도메인 엔티티PurchaseOrderItem은Long타입을 사용할 가능성이 높습니다.
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
📒 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: 날짜 필드 타입 변경 및 신규 필드 추가
requiredAt가LocalDate에서LocalDateTime으로 변경되고,expectedDeliveryAt필드가 추가되었습니다.Breaking Change 주의:
requiredAt의 타입 변경은 API 응답 형식을 변경하는 breaking change입니다:
- 변경 전:
"requiredAt": "2025-11-06"- 변경 후:
"requiredAt": "2025-11-06T12:00:00"다음 사항을 확인해주세요:
- API 소비자(프론트엔드, 다른 서비스)가 이 변경을 처리할 수 있는지 확인
- API 버전 관리 또는 마이그레이션 계획이 있는지 확인
- 날짜/시간 직렬화 형식을 명시적으로 설정하는 것을 권장합니다 (예:
@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-001→ORD-2025-001). 이는 다음과 같은 영향을 미칩니다:
- 시퀀스가 일별이 아닌 연별로 리셋됩니다
- 연간 주문량이 많을 경우 시퀀스 번호가 더 길어집니다
- 주문 코드에서 날짜 정보의 세분성이 낮아집니다
이것이 의도된 비즈니스 요구사항인지 확인해 주세요.
📝 Summary
🙏 Question & PR point
📬 Reference
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항