Skip to content

Conversation

@codrin2
Copy link
Member

@codrin2 codrin2 commented Aug 25, 2025

Issue Number

#27

As-Is

  • OrderControllerOrderQueryFacade, OrderFacade, OrderService 등 여러 계층을 혼용하며 주문 조회/생성/응답에 대한 책임이 명확히 분리되지 않았음
  • OrderItemProduct 엔티티에 직접 의존하여 JPA 연관관계로 인해 fetch depth 증가
  • OrderServiceProduct를 직접 조회하고 도메인 내부에서 재고를 차감하는 등 책임이 과도하게 집중되어 있었음
  • OrderCreateCommanduserId, shippingAddress, items만 포함하고 있어 상품 정보를 별도로 다시 조회해야 했음
  • 테스트 코드에서 categoryFixture, productFixture에 불필요한 의존성이 많고, 도메인 변경 이후 productId 기반 로직이 반영되지 않은 경우 존재
  • 주문 상세 응답 시 productName을 얻기 위해 연관 객체 탐색이 필요했고, EntityGraph fetch 범위도 넓었음

To-Be

  • OrderController에서 OrderQueryFacade를 제거하고 OrderFacade로 통합, 응답 객체는 OrderDetailBundle 기반으로 일관되게 처리
  • OrderItem에서 Product 연관관계를 제거하고 productId, priceAtPurchase를 명시적으로 보유하도록 구조 변경
  • OrderCreateCommandproductList를 추가하여 서비스 계층에서 미리 상품 정보를 주입받도록 설계 변경
  • OrderService는 외부에서 주입된 productList를 기반으로 주문을 생성하며, 내부에서 product를 조회하지 않음
  • ProductQueryService#getProductsExists 도입으로 상품 존재 여부 검증과 동시에 리스트 반환이 가능하도록 처리
  • 테스트 코드(OrderServiceTest, OrderQueryServiceTest)는 전면적으로 productList 기반 구조를 반영하고 category 제거로 간결화
  • OrderRepositoryEntityGraph 범위를 items까지만 제한하여 fetch depth 최적화
  • OrderDetailResponse.from()orderproductList를 인자로 받아 상품명과 가격 등을 표현하도록 변경

✅ Check List

  • Have all tests passed?
  • Have all commits been pushed?
  • Did you verify the target branch for the merge?
  • Did you assign the appropriate assignee(s)?
  • Did you set the correct label(s)?

📸 Test Screenshot

Additional Description

codrin2 added 15 commits August 25, 2025 20:12
…로 통합 #27

- OrderController 내부의 orderQueryFacade 의존성을 제거하고 orderFacade 단일 책임으로 통합
- placeOrder, getOrderDetail API 응답을 OrderDetailBundle 기반으로 변경하여 order와 productList 동시 반환
- OrderDetailResponse.from() 메서드 시그니처 변경에 따른 사용 방식 반영
- 주문 생성 과정에서 제품 엔터티 리스트를 함께 전달할 수 있도록 OrderCreateCommand에 productList 필드 추가
- OrderDetailResponse 생성 시 order 내부 item의 product 정보를 직접 참조하던 방식에서 productList 기반 매핑 방식으로 전환
- from 메서드에 productList를 전달받아 ID 기준으로 제품 정보를 매핑함으로써 외부 주입 방식으로 의존성 명시
- 일관된 응답 구성을 위해 productName 추출을 product 객체에서 분리하여 처리
- 일부 매핑 실패 가능성을 고려해 items 필드를 List<Item?>로 정의
…답 구조 개선 #27

- 주문 생성 시 제품 존재 여부를 검증하고, 전체 productList를 함께 반환하도록 로직 개선
- getOrderDetail 응답 시 주문에 포함된 상품 목록을 조회하여 OrderDetailBundle로 통합 반환
- OrderCreateCommand 팩토리 호출 방식 변경: userId와 OrderCreateRequest 외에 productList 추가 전달
- UserService.getUserById → checkUserExists로 의미 명확화
- OrderItem 엔티티에서 @manytoone Product 연관 매핑 제거 후 productId 필드로 대체
- 주문 생성 시점에 product.getPrice()를 호출하지 않고, 외부에서 전달받은 priceAtPurchase를 직접 주입하는 방식으로 변경
- 연관 엔티티 로딩 지연 및 복잡도 축소를 통해 성능과 응집도 개선
- findOrderByUserIdAndId → findOrderByIdAndUserId로 파라미터 순서 및 네이밍 일관성 정비
- @entitygraph에서 items.product fetch 제거하여 연관 엔티티 로딩 최소화
- Product 정보는 facade 단에서 productId 기반으로 개별 조회하도록 책임 분리
- 전체 fetch depth 감소로 성능 및 쿼리 효율성 개선
- createOrder 내부에서 Product 조회 로직 제거하고 productList를 미리 주입받아 처리하는 방식으로 리팩토링
- productList를 Map<Long, Product>으로 변환하여 productId 기준으로 빠르게 접근 가능하도록 개선
- addItemsToOrder 메서드에서 productRepository.deductStock 호출 후 재고 부족 시 ConflictException 처리
- OrderItem 생성 시 product 참조 대신 productId와 priceAtPurchase를 명시적으로 주입하여 순수 도메인 로직 유지
- 기존 checkProductsExists 메서드를 getProductsExists로 분리하여 제품 존재 여부 검증과 동시에 Product 리스트 반환
- 중복 로직 방지를 위해 checkProductsExists는 getProductsExists 내부 위임 방식으로 변경
- 주문 생성 등에서 사전 검증과 동시에 Product 리스트가 필요한 흐름을 지원하기 위한 구조 개선
- productIds 중복 제거를 위해 Set으로 변환하여 불필요한 조회 방지
- 주문 상세 응답 시 Order와 관련된 Product 목록을 함께 묶어 반환할 수 있도록 OrderDetailBundle 도입
- 주문 단건 상세 및 커서 기반 목록 조회 기능을 OrderQueryService로 분리
- findOrderByIdAndUserId 사용하여 사용자 접근 제어 포함한 상세 조회 처리
- 커서 기반 페이지네이션을 PageRequest와 함께 적용하여 성능 최적화된 주문 목록 조회 지원
- 주문 쓰기 기능과 읽기 기능을 명확히 분리하여 도메인 계층의 SRP(단일 책임 원칙) 적용
- addItemsToOrder 내부에서 productMapById 조회를 선행하여 존재하지 않는 product에 대한 사전 검증 수행
- 기존에는 재고 차감 후 product 조회하던 흐름을 검증 → 차감 순서로 변경하여 예외 메시지 정확도 향상
- IllegalStateException → IllegalArgumentException으로 예외 유형 통일성 부여
- productFixture에서 category 관련 로직 제거 및 의존성 단순화
- 기존 categoryFixture와 addCategory 호출 제거로 테스트 유연성 및 집중도 향상
- OrderCreateCommand 생성 시 productList 주입 방식에 맞춰 테스트 구조 전면 수정
- categoryFixture 제거 및 productFixture 간소화로 불필요한 도메인 의존도 축소
- OrderCreateCommand 생성 시 productList 필드 추가 요구에 따라 각 테스트 케이스에 productList 명시적으로 주입
- 기존 categoryFixture 및 productFixture(category 포함) 제거하여 테스트 의존성 최소화
- productId 기반 로직 변경에 맞춰 order.items 내부 접근 시 product → productId 기반 검증으로 수정
@coderabbitai
Copy link

coderabbitai bot commented Aug 25, 2025

Walkthrough

주문 생성 플로우가 제품 목록 기반으로 변경되었습니다. OrderItem이 Product 연관을 제거하고 productId/이름/썸네일/구매가를 비정규화 저장합니다. 조회 계층을 OrderQueryService로 통합하고 DTO를 dto 패키지로 이관/정비했습니다. 테스트 및 픽스처가 이에 맞춰 갱신되었습니다.

Changes

Cohort / File(s) Summary of Changes
Order 생성 커맨드/파사드
src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt, src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt
OrderCreateCommand에 productList: List<Product> 추가 및 toCommand 시그니처 변경. OrderFacade에 ProductQueryService/OrderQueryService 주입, placeOrder에서 제품 존재 조회 후 커맨드 생성. 주문 상세/커서 조회 메서드 추가. DTO import 경로 변경(dto.*).
주문 도메인: 아이템/서비스/리포지토리/쿼리서비스
src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt, .../OrderService.kt, .../OrderRepository.kt, .../OrderQueryService.kt
OrderItem이 Product 연관 제거, productId, name, thumbnailUrl, priceAtPurchase 저장. newOrderItem 시그니처 갱신. OrderService는 productList 맵으로 아이템 생성 및 재고 차감, V2/V3 생성 메서드 제거. OrderRepository 메서드명/파라미터 순서 변경 및 EntityGraph 단순화. OrderQueryFacade→OrderQueryService로 이동/개명, 상세 조회 시그니처/예외 처리 변경.
주문 프리젠테이션/DTO/스웨거
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt, .../presentation/dto/OrderCreateRequest.kt, .../presentation/dto/OrderDetailResponse.kt, .../presentation/dto/OrderSummary.kt, .../presentation/swagger/OrderSwagger.kt
컨트롤러가 Facade 단일 의존으로 통합, V3 엔드포인트 제거. 요청/응답 DTO를 dto 패키지로 이동/신규 추가: OrderDetailResponse는 OrderItem의 비정규화 필드 사용, OrderListItem→OrderSummary로 개명. Swagger 예시/반환 타입 갱신(썸네일 포함, Summary 적용).
상품 쿼리 서비스
src/main/kotlin/com/dh/baro/product/domain/service/ProductQueryService.kt
getProductsExists(productIds) 추가(존재 검증 후 리스트 반환). 기존 checkProductsExists는 신규 메서드 위임.
주문 테스트 리팩터링
src/test/kotlin/com/dh/baro/order/domain/OrderServiceTest.kt, .../OrderQueryServiceTest.kt, .../order/Fixture.kt
카테고리 의존 제거, 제품 중심 픽스처로 전환. OrderCreateCommand에 productList 반영. OrderItem 검증을 productId 기준으로 수정.
룩/상품 테스트 픽스처/임포트 정리
src/test/kotlin/com/dh/baro/look/Fixture.kt, .../look/domain/LookReactionServiceTest.kt, .../look/domain/LookServiceTest.kt, src/test/kotlin/com/dh/baro/product/Fixture.kt, .../product/domain/ProductQueryServiceTest.kt
패키지 경로 정리 및 픽스처 임포트 수정(lookFixture/productFixture/categoryFixture). 테스트 로직 변화 없음.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant C as OrderController
  participant F as OrderFacade
  participant USvc as UserService
  participant PQry as ProductQueryService
  participant S as OrderService
  participant R as OrderRepository

  U->>C: POST /orders (OrderCreateRequest)
  C->>F: placeOrder(userId, request)
  F->>USvc: checkUserExists(userId)
  USvc-->>F: exists/ok
  F->>PQry: getProductsExists(request.itemIds)
  PQry-->>F: List<Product>
  F->>S: createOrder(OrderCreateCommand(userId, productList, request))
  S->>S: map products by id
  S->>R: save(Order with items built from productMap + request)
  R-->>S: Order
  S-->>F: Order
  F-->>C: Order
  C-->>U: 201 Created (OrderDetailResponse)
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant C as OrderController
  participant F as OrderFacade
  participant Q as OrderQueryService
  participant R as OrderRepository

  U->>C: GET /orders/{orderId}
  C->>F: getOrderDetail(userId, orderId)
  F->>Q: getOrderDetailByUserId(orderId, userId)
  Q->>R: findByIdAndUserId(orderId, userId)
  alt found
    R-->>Q: Order
    Q-->>F: Order
    F-->>C: Order
    C-->>U: 200 OK (OrderDetailResponse)
  else not found
    R-->>Q: null
    Q-->>F: IllegalArgumentException(ORDER_NOT_FOUND)
    F-->>C: error
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#27

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @coderabbitai 요약 to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 3

🧹 Nitpick comments (19)
src/main/kotlin/com/dh/baro/product/domain/service/ProductQueryService.kt (2)

23-25: 오류 메시지에 ‘누락된 ID’만 넣고, 반환 리스트의 순서를 입력 대비 안정적으로 유지하세요

  • 현재는 require(products.size == distinctIds.size) 실패 시 전체 productIds를 메시지에 넣습니다. 어떤 ID가 누락됐는지 바로 파악할 수 있도록 차집합을 사용하세요.
  • findAllById는 반환 순서를 보장하지 않습니다. 입력된 ID 순서(중복 제거 후)로 정렬해 반환하면 디버깅과 추적성이 좋아집니다.

적용 제안(diff):

-        require(products.size == distinctIds.size) {
-            ErrorMessage.PRODUCT_NOT_FOUND.format(productIds)
-        }
+        val fetchedIds = products.map { it.id }.toSet()
+        val missingIds = distinctIds - fetchedIds
+        require(missingIds.isEmpty()) {
+            ErrorMessage.PRODUCT_NOT_FOUND.format(missingIds)
+        }

-        return products
+        // 입력 ID의 논리적 순서를 유지(중복은 제거된 상태 유지)
+        val productsById = products.associateBy { it.id }
+        return productIds.distinct().map { productsById.getValue(it) }

Also applies to: 27-28


30-31: 중복 역할의 래퍼 메서드 네이밍/문서화 제안

checkProductsExists는 반환값을 버리는 래퍼입니다. 호출 의도를 더 명확히 하려면 ensureProductsExist 같은 이름이나 KDoc에 “존재하지 않으면 예외”를 명시하는 것을 고려하세요.

src/main/kotlin/com/dh/baro/order/application/OrderDetailBundle.kt (1)

6-9: 필드 명명 일관성 제안: productListproducts

동일한 패턴의 번들들(ProductDetailBundle.products, LookDetailBundle.products)과 맞추면 가독성과 검색성이 좋아집니다. 변경 시 호출부 전체에 전파되므로 범위를 고려해 선택하세요.

적용 제안(diff):

-data class OrderDetailBundle (
-    val order: Order,
-    val productList: List<Product>,
-)
+data class OrderDetailBundle (
+    val order: Order,
+    val products: List<Product>,
+)
src/main/kotlin/com/dh/baro/order/presentation/OrderDetailResponse.kt (2)

27-27: 변수 명명 가독성

productMapByIds보다는 productsById가 더 자연스럽습니다(“id→product 매핑”을 표현).


39-39: 주문 상세는 “구매 시점 스냅샷”이 이상적 — productNameAtPurchase 도입 고려

현재 이름은 product.getName()(현재 시점)을 사용합니다. 상품명이 변경/삭제되면 주문 상세가 시간에 따라 달라집니다. priceAtPurchase와 동일하게 productNameAtPurchase(또는 displayNameAtPurchase)를 OrderItem에 추가 저장하면 스냅샷 일관성이 보장됩니다.

src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (3)

15-27: 불변 모델 의도를 매핑에도 반영해 주세요 (updatable=false 권장)

현재 필드들이 모두 val로 불변이지만 JPA 매핑에는 updatable=false가 없습니다. 의도를 명확히 하고 쓸데없는 UPDATE SQL을 방지하기 위해 다음 컬럼들에 updatable=false를 권장합니다: order_id, product_id, quantity, price_at_purchase.

다음과 같이 적용 가능합니다:

 @ManyToOne(fetch = FetchType.LAZY)
-@JoinColumn(name = "order_id", nullable = false)
+@JoinColumn(name = "order_id", nullable = false, updatable = false)
 val order: Order,

-@Column(name = "product_id", nullable = false)
+@Column(name = "product_id", nullable = false, updatable = false)
 val productId: Long,

-@Column(name = "quantity", nullable = false)
+@Column(name = "quantity", nullable = false, updatable = false)
 val quantity: Int,

-@Column(name = "price_at_purchase", nullable = false, precision = 10, scale = 0)
+@Column(name = "price_at_purchase", nullable = false, precision = 10, scale = 0, updatable = false)
 val priceAtPurchase: BigDecimal,

8-10: 중복 아이템 방지 정책이 서비스 레벨에만 있다면, DB 제약도 고려해 주세요

서비스에서 mergeDuplicateItems로 중복을 합치고 있지만, 다른 경로로 삽입될 여지를 차단하려면 (order_id, product_id) 유니크 제약을 추가하는 방안을 검토해 주세요. 비즈니스 요구가 “한 주문에 상품 1건당 1개의 아이템”이라면 다음과 같이 DDL 제약으로 보강하는 게 안전합니다.

-@Table(name = "order_items")
+@Table(
+  name = "order_items",
+  uniqueConstraints = [UniqueConstraint(columnNames = ["order_id", "product_id"])]
+)

주의: 동일 상품을 여러 라인으로 허용해야 한다면 본 제약은 추가하지 마십시오.


35-47: 팩토리에서 도메인 불변식 보강 (수량/가격 유효성)

입력 검증이 상위 레이어에 있더라도, 엔티티 팩토리에서 핵심 불변식을 한 번 더 수호하는 것이 안전합니다.

다음처럼 가벼운 require를 추가해 주세요:

 companion object {
   fun newOrderItem(
     order: Order,
     productId: Long,
     quantity: Int,
     priceAtPurchase: BigDecimal,
   ): OrderItem =
-    OrderItem(
+    run {
+      require(quantity > 0) { "quantity must be positive: $quantity" }
+      require(priceAtPurchase >= BigDecimal.ZERO) { "priceAtPurchase must be >= 0: $priceAtPurchase" }
+      OrderItem(
         id = IdGenerator.generate(),
         order = order,
         productId = productId,
         quantity = quantity,
         priceAtPurchase = priceAtPurchase,
-    )
+      )
+    }
 }
src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (1)

20-31: 커서 페이지 사이즈 가드 추가 제안

API 입력으로 전달되는 size에 상한을 두지 않으면 과도한 조회를 유발할 수 있습니다. 1..50 또는 1..100 범위로 coerceIn을 적용하세요.

-    ): Slice<Order> {
-        val pageable = PageRequest.of(0, size)
+    ): Slice<Order> {
+        val pageSize = size.coerceIn(1, 50)
+        val pageable = PageRequest.of(0, pageSize)
         return orderRepository.findByUserIdAndCursorId(
             userId = userId,
             cursorId = cursorId,
             pageable = pageable,
         )
src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (2)

18-27: productList → 맵 변환 시 중복 ID 검출 및 사전 검증을 추가해 주세요

associateBy는 마지막 값을 취해 중복을 숨깁니다. productList에 동일 ID가 들어오면 묵살되어 디버깅이 어려워집니다. 또한 DB write 전에 items의 유효성을 전체 검증하면 불필요한 write/rollback을 줄일 수 있습니다.

다음과 같이 두 가지 체크를 권장합니다.

  • productList 내 ID 중복 검사
  • 모든 item.productId가 productMap에 존재하는지 사전 검증
     fun createOrder(cmd: OrderCreateCommand): Order {
         val order = Order.newOrder(cmd.userId, cmd.shippingAddress)
-        val productMapById = cmd.productList.associateBy { it.id }
+        val productMapById = cmd.productList.associateBy { it.id }
+        require(productMapById.size == cmd.productList.size) {
+            "Duplicate product IDs detected in productList"
+        }
+
+        // fail-fast: 모든 아이템의 상품 존재 여부 선검증
+        cmd.items.forEach { item ->
+            if (productMapById[item.productId] == null) {
+                throw IllegalArgumentException(ErrorMessage.PRODUCT_NOT_FOUND.format(item.productId))
+            }
+        }
 
         val mergedItems = mergeDuplicateItems(cmd.items)
         mergedItems.forEach { addItemsToOrder(it, order, productMapById) }
         order.updateTotalPrice()
 
         return orderRepository.save(order)
     }

39-47: require 블록 남용으로 가독성 저하 — 명시적 조건 분기 사용 권장

require(updated != 0) { throw ConflictException(...) }는 의도 전달이 어렵습니다. 실패 시 특정 예외를 던질 목적이면 단순 if 분기가 더 명확합니다.

-        val updated = productRepository.deductStock(item.productId, item.quantity)
-
-        require(updated != 0) {
-            throw ConflictException(ErrorMessage.OUT_OF_STOCK.format(item.productId))
-        }
+        val updated = productRepository.deductStock(item.productId, item.quantity)
+        if (updated == 0) {
+            throw ConflictException(ErrorMessage.OUT_OF_STOCK.format(item.productId))
+        }
src/test/kotlin/com/dh/baro/order/domain/OrderServiceTest.kt (2)

95-103: 항목 가격 보존(priceAtPurchase)와 소계 검증도 함께 확인해 주세요

도메인 리팩터의 핵심인 priceAtPurchase 저장과 계산이 올바른지 테스트에서 보강하면 회귀를 막는 데 효과적입니다.

다음 단언을 추가해 보세요:

 it("항목 개수와 수량이 정확하다") {
     order.items.shouldHaveSize(2)
     order.items.first { it.productId == p1.id }.quantity shouldBe 2
 }
+
+it("각 항목의 구매시 가격과 소계가 정확하다") {
+    val i1 = order.items.first { it.productId == p1.id }
+    val i2 = order.items.first { it.productId == p2.id }
+    i1.priceAtPurchase shouldBe BigDecimal("1000")
+    i2.priceAtPurchase shouldBe BigDecimal("2000")
+    i1.subTotal() shouldBe BigDecimal("2000")
+    i2.subTotal() shouldBe BigDecimal("2000")
+}

160-165: 중복 상품 합산 시 총액도 검증해 주세요

현재 라인 수/재고만 확인하고 있습니다. 총금액이 (1+2)*1000=3000으로 반영되는지도 함께 검증하면 merge 로직과 totalPrice 계산을 한 번에 커버할 수 있습니다.

 it("각 항목을 그대로 저장하고 합산하여 재고를 차감한다") {
     val order = orderService.createOrder(cmd)

     order.items.shouldHaveSize(1) // p1 하나만
     productRepository.findById(p1.id).get().getQuantity() shouldBe 2 // 5-(1+2)
+    order.totalPrice shouldBe BigDecimal("3000")
 }
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2)

24-26: 반복되는 매핑을 줄이는 경량 헬퍼를 고려해 주세요

OrderDetailResponse.from(order, productList)를 두 곳에서 반복합니다. 응답 팩토리에 번들 오버로드를 추가해 중복을 제거하면 읽기성이 좋아집니다.

예시:

- return OrderDetailResponse.from(orderDetailBundle.order, orderDetailBundle.productList)
+ return OrderDetailResponse.from(orderDetailBundle)
// OrderDetailResponse.kt (추가)
companion object {
    fun from(bundle: OrderDetailBundle): OrderDetailResponse =
        from(bundle.order, bundle.productList)
}

Also applies to: 34-36


45-51: 조회 page size에 상한 적용 고려

size 기본값은 10이지만, 과도한 값이 들어오면 부담이 큽니다. Controller 레이어에서 coerceIn(1, 50) 등 상한을 걸거나, Facade/Service에서 방어 로직을 추가해 주세요.

src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (1)

28-31: getOrderDetail: 상품 재조회 시 중복 제거로 불필요한 조회 최소화

주문 상세에서 상품명을 매핑하기 위해 재조회하는 흐름은 합리적입니다. 동일 상품이 여러 항목에 반복될 수 있으므로 ID를 distinct()로 한 번만 조회하면 쿼리를 줄일 수 있습니다. 과거 주문의 상품이 삭제된 경우도 있을 수 있어 여기서 하드 에러를 내지 않는 현재 방향이 실무적으로 적절합니다(응답 매퍼에서 가격/명칭 fallback 처리 권장).

다음과 같이 경미한 리팩터를 권장합니다.

-        val productList = productQueryService.getProductsExists(order.items.map { orderItem -> orderItem.productId })
+        val productIds = order.items.map { it.productId }.distinct()
+        val productList = productQueryService.getProductsExists(productIds)

OrderDetailResponse.from(order, productList)가 매칭 실패 시(상품 삭제 등) priceAtPurchase 등으로 graceful fallback 하는지 한 번만 점검 부탁드립니다.

src/test/kotlin/com/dh/baro/order/domain/OrderQueryServiceTest.kt (3)

24-26: DataJpaTest에서는 @import가 더 자연스럽습니다

@ContextConfiguration로 테스트 컨텍스트를 전면 구성하기보다, DataJpaTest 슬라이스에 필요한 빈만 주입하려면 @import(OrderService, OrderQueryService)가 권장됩니다. 현재도 동작할 수 있으나 슬라이스 설정을 침범하지 않는 @import가 더 일관적입니다.

아래처럼 변경을 제안합니다.

-@ContextConfiguration(classes = [OrderService::class, OrderQueryService::class])
+@Import(OrderService::class, OrderQueryService::class)

파일 상단에 다음 import 추가가 필요합니다.

import org.springframework.context.annotation.Import

47-70: 픽스처 저장 saveAll로 간결화 및 성능 미세 개선

테스트에서 큰 차이는 없지만, 가독성과 일관성을 위해 save 호출을 묶을 수 있습니다.

-        productRepository.save(p1)
-        productRepository.save(p2)
-        productRepository.save(p3)
+        productRepository.saveAll(listOf(p1, p2, p3))

덧붙여, 고정 ID(11,12,13) 사용은 “불변 id 필드 직접 접근 선호”라는 과거 피드백과 부합합니다(학습 메모 참고). 현재 형태 유지해도 무방합니다.


113-138: 중복되는 주문 생성 코드 DRY 제안

o1/o2/o3 생성이 거의 동일합니다. 로컬 헬퍼로 중복을 줄이면 가독성이 좋아집니다.

예시:

private fun createSimpleOrder(product: Product, addr: String = "addr") =
    OrderCreateCommand(
        userId = USER_ID,
        productList = listOf(product),
        shippingAddress = addr,
        items = listOf(OrderCreateCommand.Item(product.id, 1))
    )

사용:

o1 = orderService.createOrder(createSimpleOrder(p1))
o2 = orderService.createOrder(createSimpleOrder(p2))
o3 = orderService.createOrder(createSimpleOrder(p1))
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3a2cb and 1e47da7.

📒 Files selected for processing (13)
  • src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/application/OrderDetailBundle.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderRepository.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (3 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/OrderDetailResponse.kt (3 hunks)
  • src/main/kotlin/com/dh/baro/product/domain/service/ProductQueryService.kt (1 hunks)
  • src/test/kotlin/com/dh/baro/order/Fixture.kt (0 hunks)
  • src/test/kotlin/com/dh/baro/order/domain/OrderQueryServiceTest.kt (7 hunks)
  • src/test/kotlin/com/dh/baro/order/domain/OrderServiceTest.kt (7 hunks)
💤 Files with no reviewable changes (1)
  • src/test/kotlin/com/dh/baro/order/Fixture.kt
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: codrin2
PR: S-BARO/server#17
File: src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt:38-59
Timestamp: 2025-08-12T13:52:29.449Z
Learning: codrin2 prefers direct field access for immutable id properties (val) rather than using getters, reasoning that immutable fields pose less encapsulation risk.
🧬 Code graph analysis (11)
src/main/kotlin/com/dh/baro/order/application/OrderDetailBundle.kt (3)
src/main/kotlin/com/dh/baro/product/application/dto/ProductDetailBundle.kt (1)
  • product (6-9)
src/main/kotlin/com/dh/baro/look/application/dto/LookDetailBundle.kt (1)
  • look (7-12)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (4)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderCreateRequest.kt (1)
  • message (18-24)
src/main/kotlin/com/dh/baro/look/domain/LookProduct.kt (1)
  • name (6-42)
src/main/kotlin/com/dh/baro/product/domain/Product.kt (1)
  • name (11-135)
src/main/kotlin/com/dh/baro/order/domain/OrderRepository.kt (2)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (2)
  • readOnly (11-33)
  • getOrderDetail (17-19)
src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (2)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderCreateRequest.kt (1)
  • message (18-24)
src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (2)
src/main/kotlin/com/dh/baro/product/application/ProductFacade.kt (1)
  • storeService (12-51)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (1)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderDetailResponse.kt (2)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/product/presentation/dto/ProductDetail.kt (1)
  • id (7-28)
src/test/kotlin/com/dh/baro/order/domain/OrderQueryServiceTest.kt (1)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (2)
src/test/kotlin/com/dh/baro/order/application/OrderQueryFacadeTest.kt (2)
  • classes (30-160)
  • orderQueryFacade (79-79)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (2)
  • readOnly (11-33)
  • getOrderDetail (17-19)
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
src/main/kotlin/com/dh/baro/product/presentation/ProductController.kt (1)
  • productFacade (15-76)
src/test/kotlin/com/dh/baro/order/domain/OrderServiceTest.kt (1)
src/test/kotlin/com/dh/baro/order/Fixture.kt (1)
  • productFixture (6-22)
🔇 Additional comments (15)
src/main/kotlin/com/dh/baro/product/domain/service/ProductQueryService.kt (1)

19-28: 제품 존재 검증을 리스트 반환으로 확장한 방향 좋습니다

중복 제거 후 조회하고, 개수로 일치성 검증한 뒤 리스트를 반환하는 구성은 호출부 활용도를 높입니다.

src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (1)

4-4: OrderCreateCommand.toCommand 호출부 점검 및 안전성 검증 완료

  • OrderCreateCommand.toCommand(userId, productList, request)OrderFacade.placeOrder에서만 호출되며, 이미 새로운 파라미터 시그니처가 반영되어 있습니다.
  • 제안하신 require(requestedIds == providedIds) 검증 로직도 메서드 내부에 정상 추가되어 있어, 요청된 상품 ID 집합과 조회된 상품 ID 집합 불일치로 인한 매핑 오류를 방지할 수 있습니다.
  • 그 외 다른 호출부(예: LookFacadegetAllByIds)는 toCommand를 사용하지 않으므로 추가 변경이 필요 없습니다.

위 사항 확인하였으며, 별도 개선 사항은 없습니다.

src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (2)

19-21: 연관 제거 방향성 훌륭합니다 — fetch depth/복잡도 감소에 기여

Product 연관을 productId 컬럼으로 치환한 선택이 조회 성능과 사이드이펙트 제어에 유리합니다. 이후 조회시 필요 데이터는 상위 계층에서 주입하므로 단방향 의존이 깔끔해졌습니다.


31-33: 금액 스케일 일관성 확인

subTotal은 scale 조정을 하지 않고 BigDecimal 곱셈 결과를 반환합니다. Order.updateTotalPrice에서 setScale(0, HALF_UP)으로 정규화하므로 전체 합계 기준으로는 문제 없습니다. 다만 항목 단위 금액을 별도 응답/로그에 사용한다면 동일 스케일 규칙을 적용할지 결정이 필요합니다.

정책이 “항목 소계도 0 스케일”이라면 여기서도 setScale(0, HALF_UP)을 적용해 주세요.

src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (1)

20-21: 불변 id 직접 접근 사용 👍

productMapById 구성 시 Product.id를 직접 참조하는 현재 방식은 (불변 id인 경우) 캡슐화 위험이 낮고 간결합니다. 과거 선호사항과도 일치합니다.

src/test/kotlin/com/dh/baro/order/domain/OrderServiceTest.kt (2)

44-67: 테스트 픽스처 구성 깔끔합니다

카테고리 의존 제거, productFixture로 테스트 최소화를 잘 하셨습니다. 재고/가격/ID를 명시적으로 제어할 수 있어 시나리오 표현력이 좋아졌습니다.


113-131: 롤백 단언 훌륭합니다

OUT_OF_STOCK 시나리오에서 Order 미저장/재고 복구를 동시에 확인하는 것은 트랜잭션 경계 검증에 유용합니다. 유지해 주세요.

src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (1)

18-26: Facade 단일 의존으로 정리된 컨트롤러 👍

placeOrder가 OrderDetailBundle을 받아 응답 DTO로 변환하는 흐름이 명확합니다. QueryFacade 제거로 책임이 응집되었습니다.

src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (4)

15-18: 의존성 구성 깔끔: Facade에 조회/명령 경로를 일원화한 점 좋습니다

OrderService와 OrderQueryService, ProductQueryService를 함께 주입해 생성/조회 흐름을 Facade에서 조립하도록 한 설계가 PR 목표(레이어 책임 정리)와 일치합니다.


34-44: getOrdersByCursor 위임 적절

Facade가 페이징/커서 로직을 도메인 조회 서비스로 위임하는 구조 적절합니다. 응답이 Slice인 점도 호출측에 hasNext 등 판단 여지를 남겨 유연합니다.


20-26: 검증 완료: OrderService.createOrder은 트랜잭션 적용 대상입니다.

OrderFacade에서 @transactional을 제거해도 OrderService 클래스가 클래스 레벨로 @transactional을 선언하고 있어 createOrder 메소드가 트랜잭션 경계 내에서 원자적으로 실행됩니다.
아래 위치에서 확인되었습니다:

  • src/main/kotlin/com/dh/baro/order/domain/OrderService.kt
    • 라인 12: @Transactional 클래스 레벨 선언
    • 라인 18: fun createOrder(...) 메소드 정의

추가 조치가 필요하지 않습니다.


20-26: getProductsExists 내부에서 이미 중복 제거 및 검증 처리 중—추가 방어 로직 불필요
ProductQueryService.getProductsExists 메서드에서

  • 입력된 productIds를 Set으로 변환해 중복을 제거하고
  • 조회 결과(products.size)와 요청 ID 개수(distinctIds.size)가 다를 경우 require로 예외를 발생시킵니다

따라서 placeOrder에서 제안한 별도 중복 제거·불일치 검증 로직은 중복 검증이며, 추가 구현 없이 현재 로직만으로 충분합니다.

해당 구현 위치

  • src/main/kotlin/com/dh/baro/product/domain/service/ProductQueryService.kt 라인 19–23

Likely an incorrect or invalid review comment.

src/test/kotlin/com/dh/baro/order/domain/OrderQueryServiceTest.kt (3)

81-94: 주문 상세 단건 조회 시나리오 충실

OrderCreateCommand에 productList를 주입하고, 조회는 service 레벨에서 수행하도록 한 흐름이 새 도메인 설계와 일관됩니다. 예외 없이 로드되는지 확인하고, 핵심 필드(productId)만 명확히 검증하는 단정도 적절합니다.


169-177: 빈 내역 케이스 처리까지 커버된 점 좋습니다

다른 사용자에 대한 비어있는 slice 검증이 있어 경계 조건까지 잘 커버됩니다.


149-156: 정렬 조건 검증 완료

OrderQueryServiceTest에서 검증한 id desc 정렬과 hasNext() 판별 로직이 의도대로 동작하며, 실제 저장소 레벨의 findByUserIdAndCursorId 쿼리에도 다음과 같이 order by o.id desc가 명시되어 있습니다. 추가 조치 불필요합니다.

  • src/main/kotlin/com/dh/baro/order/domain/OrderRepository.kt (라인 16–18):
    and (:cursorId is null or o.id < :cursorId)
    order by o.id desc

val totalPrice: BigDecimal,
val orderedAt: Instant?,
val items: List<Item>,
val items: List<Item?>,
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

응답 items에 null 원소 허용은 API 사용성을 크게 떨어뜨립니다 — null 제거 또는 일관된 대체값 반환을 권장

현재 items: List<Item?>로 선언하고, 매핑에서 상품을 찾지 못하면 null을 넣습니다. 이는

  • 클라이언트에서 불필요한 null 체크를 강제하고,
  • 스키마 안정성을 해칩니다(브레이킹 체인지).

추천:

  • 리스트를 List<Item>로 유지하고, 상품이 없어도 대체 이름(예: “삭제된 상품”)을 넣어 일관된 스키마를 보장하거나,
  • 혹은 mapNotNull로 필터링하여 null 자체를 제거하세요.

또한 Order.itemsMutableSet이므로 응답 순서를 보장하려면 정렬을 권장합니다(예: productId 기준).

적용 제안(diff):

-    val items: List<Item?>,
+    val items: List<Item>,
@@
-        fun from(order: Order, productList: List<Product>): OrderDetailResponse {
-            val productMapByIds = productList.associateBy { it.id }
+        fun from(order: Order, productList: List<Product>): OrderDetailResponse {
+            val productsById = productList.associateBy { it.id }
@@
-                items = order.items.map {
-                    productMapByIds[it.productId]?.let { product ->
-                        Item(
-                            productId = it.productId,
-                            productName = product.getName(),
-                            quantity = it.quantity,
-                            priceAtPurchase = it.priceAtPurchase,
-                        )
-                    }
-                }
+                items = order.items
+                    .sortedBy { it.productId }
+                    .map { item ->
+                        val product = productsById[item.productId]
+                        Item(
+                            productId = item.productId,
+                            productName = product?.getName() ?: "삭제된 상품",
+                            quantity = item.quantity,
+                            priceAtPurchase = item.priceAtPurchase,
+                        )
+                    }

Also applies to: 35-44

🤖 Prompt for AI Agents
In src/main/kotlin/com/dh/baro/order/presentation/OrderDetailResponse.kt around
lines 15 and 35-44, the response currently exposes items as List<Item?> which
forces clients to handle nulls and destabilizes the schema; change the type to
List<Item> and update the mapping to never emit nulls by either substituting a
sentinel Item (e.g., name="삭제된 상품") when a product is missing or by using
mapNotNull to filter missing products, and because Order.items is a MutableSet
ensure you produce a stable order by sorting the resulting list (for example by
productId) before returning.

@S-BARO S-BARO deleted a comment from coderabbitai bot Aug 26, 2025
@S-BARO S-BARO deleted a comment from coderabbitai bot Aug 26, 2025
- items 리스트에서 nullable Item 제거 (List<Item?> → List<Item>) 및 mapNotNull 처리로 응답 안정성 강화
- 항목 정렬을 productId 기준으로 추가하여 응답 일관성 확보
Copy link

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (1)

20-31: size 유효성 검증 및 상한 적용이 필요합니다.

외부 입력 size에 상한이 없으면 큰 값으로 인한 부하 위험이 있습니다. 서비스 레이어에서 최소/최대 범위를 강제하세요.

다음처럼 보완을 제안합니다:

     fun getOrdersByCursor(
         userId: Long,
         cursorId: Long?,
         size: Int,
     ): Slice<Order> {
-        val pageable = PageRequest.of(0, size)
+        require(size > 0) { "size must be positive" }
+        val limited = size.coerceAtMost(MAX_PAGE_SIZE)
+        val pageable = PageRequest.of(0, limited)
         return orderRepository.findByUserIdAndCursorId(
             userId = userId,
             cursorId = cursorId,
             pageable = pageable,
         )
     }

아래와 같이 상수를 추가해 주세요(파일 내 적절한 위치, 예: 클래스 하단):

private const val MAX_PAGE_SIZE = 50
🧹 Nitpick comments (18)
src/test/kotlin/com/dh/baro/look/Fixture.kt (2)

11-21: Kotlin 스타일로 간결화: apply 사용 제안

객체 생성 후 메서드 호출을 apply로 묶으면 가독성이 좋아지고 임시 변수 제거가 가능합니다.

-    val look = Look(
-        id = id,
-        creatorId = 1L,
-        title = title,
-        description = null,
-        thumbnailUrl = "thumb://$id",
-    )
-    look.addImages(imageUrls)
-    look.addProducts(productIds)
-    return look
+    return Look(
+        id = id,
+        creatorId = 1L,
+        title = title,
+        description = null,
+        thumbnailUrl = "thumb://$id",
+    ).apply {
+        addImages(imageUrls)
+        addProducts(productIds)
+    }

5-10: 테스트 편의성 향상: 기본값 추가(옵션)

자주 쓰이는 케이스에 기본값을 주면 호출부가 단순해지고 중복을 줄일 수 있습니다. Java 테스트가 있다면 @JvmOverloads도 고려해 주세요.

-fun lookFixture(
-    id: Long,
-    title: String,
-    imageUrls: List<String>,
-    productIds: List<Long>,
-): Look {
+@JvmOverloads
+fun lookFixture(
+    id: Long = 1L,
+    title: String = "title-$id",
+    imageUrls: List<String> = emptyList(),
+    productIds: List<Long> = emptyList(),
+): Look {
src/test/kotlin/com/dh/baro/product/domain/ProductQueryServiceTest.kt (7)

21-21: DataJpaTest에는 @import가 더 적합

@ContextConfiguration은 슬라이스 테스트의 의도(최소 컨텍스트)와 다르게 동작할 수 있습니다. 서비스 빈 추가 목적이라면 @Import(ProductQueryService::class)가 관용적입니다.

다음 패치를 제안합니다:

-import org.springframework.test.context.ContextConfiguration
+import org.springframework.context.annotation.Import
@@
-@ContextConfiguration(classes = [ProductQueryService::class])
+@Import(ProductQueryService::class)

Also applies to: 25-25


38-39: 불필요한 세미콜론 제거

Kotlin 스타일 가이드에 맞게 세미콜론을 제거하면 가독성이 좋아집니다.

-    lateinit var p1: Product;
-    lateinit var p2: Product;
+    lateinit var p1: Product
+    lateinit var p2: Product

65-65: Slice 크기 검증은 content 기준으로 표현하는 편이 명확

Slice.size는 요청 페이지 크기(getSize) 의미입니다. 실제 반환된 요소 수를 의도했다면 content.size가 더 직관적입니다. (동일 결과지만 의도 전달 측면)

-                result.size shouldBe requestedSize
+                result.content.size shouldBe requestedSize

71-71: 가능하면 프로퍼티 직접 접근으로 통일

likesCount가 공개 불변 프로퍼티(val)라면 게터 호출 대신 프로퍼티 접근을 권장합니다. 지난 PR에서 id와 같은 불변 값은 필드 접근을 선호하신다고 하셔서 스타일을 맞추면 좋겠습니다.

-            val cursorLikes = p1.getLikesCount()
+            val cursorLikes = p1.likesCount
@@
-                    cursorLikes = lastItem.getLikesCount(),
+                    cursorLikes = lastItem.likesCount,

Also applies to: 184-184


51-51: 정리 로직은 롤백에 위임하거나 대칭적으로 정리

@DataJpaTest는 각 테스트를 트랜잭션으로 감싸고 기본 롤백합니다. 따라서 명시적 deleteAll()은 보통 불필요합니다. 유지하려면 categoryRepository.deleteAll()도 함께 호출해 대칭적으로 정리하는 편이 깔끔합니다.

롤백에 위임하려면 다음과 같이 제거할 수 있습니다.

-    afterEach { productRepository.deleteAll() }

143-149: fetch join 보장을 더 엄밀하게 검증

현재 검증은 트랜잭션 내에서 접근 가능함만 보장합니다. fetch join(즉시 로딩) 자체를 검증하려면 로딩 여부를 확인하거나 영속성 컨텍스트를 비운 뒤 추가 쿼리 없이 접근되는지를 확인하는 방식이 더 명확합니다.

예시(아이디어):

  • EntityManagerFactory 주입 후 persistenceUnitUtil.isLoaded(detail, "images") / "productCategories"로 확인
  • 또는 Hibernate.isInitialized(detail.images) 사용
// 클래스 생성자에 추가 주입
private val entityManagerFactory: EntityManagerFactory,

// 테스트 내
val detail = productQueryService.getProductDetail(p1.id)
val puu = entityManagerFactory.persistenceUnitUtil
puu.isLoaded(detail, "images") shouldBe true
puu.isLoaded(detail, "productCategories") shouldBe true

원하시면 쿼리 카운터(DataSourceProxy 등)를 붙이는 버전으로도 보강해 드릴게요.


53-69: 동일 좋아요 수에 대한 tie-break 테스트 추가 제안

정렬 조건이 (likes DESC, id DESC)라면 동일 likes에서 id로 안정 정렬되는지를 검증하는 케이스가 있으면 회귀 방지에 유용합니다.

원하시면 top 카테고리에 likes=300인 또 다른 상품을 추가하고, 커서(likes=300, id=p1.id) 기준에서 기대 id가 정확히 필터링되는 테스트 코드를 드리겠습니다.

Also applies to: 70-84

src/test/kotlin/com/dh/baro/product/Fixture.kt (1)

31-38: 리플렉션으로 createdAt 세팅은 취약 — 헬퍼로 감싸기 권장

필드명/상속구조 변경에 민감합니다. 테스트 안정성을 위해 테스트 전용 헬퍼로 감싸거나 도메인에 테스트 전용 빌더/팩토리를 두는 방식을 권장합니다.

예시(헬퍼로 캡슐화):

internal fun setCreatedAtForTest(product: Product, daysAgo: Long) {
    val field = product.javaClass.superclass!!.getDeclaredField("createdAt")
    field.isAccessible = true
    field.set(product, Instant.now().minus(daysAgo, ChronoUnit.DAYS))
}

그리고 본문은:

if (createdAtAgoDays > 0) {
    setCreatedAtForTest(product, createdAtAgoDays)
}

원하시면 @TestEntityFactory 스타일로 확장해 드립니다.

src/main/kotlin/com/dh/baro/order/presentation/dto/OrderCreateRequest.kt (1)

3-7: 리스트 비어있음 검증은 @notempty가 의도와 더 가깝습니다

목록이 최소 1개 이상이어야 함을 표현할 때는 @SiZe(min=1)보다 @notempty가 의미상 직관적입니다. 메시지는 동일하게 유지할 수 있습니다.

다음과 같이 변경을 제안합니다:

 import jakarta.validation.Valid
 import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.NotEmpty
 import jakarta.validation.constraints.Positive
 import jakarta.validation.constraints.Size
@@
-    @field:Size(min = 1, message = "주문 항목이 비어 있습니다.")
+    @field:NotEmpty(message = "주문 항목이 비어 있습니다.")
     @field:Valid
     val orderItems: List<OrderItem>,

Also applies to: 13-15

src/main/kotlin/com/dh/baro/order/presentation/swagger/OrderSwagger.kt (1)

135-165: SliceResponse의 제네릭 타입이 스키마에 반영되지 않을 수 있습니다

현재 Content(schema = Schema(implementation = SliceResponse::class))로 명시하여, 생성된 문서에서 content 항목의 제네릭 파라미터(OrderSummary)가 드러나지 않을 수 있습니다. 다음 중 한 가지로 개선을 권장합니다.

  • 선택 1: responses.content의 schema 지정을 제거하고, 반환 타입(SliceResponse)로부터 springdoc가 제네릭을 추론하게 둡니다.
  • 선택 2: 예제 전용 래퍼 DTO(예: OrdersSliceExample)를 두고 이를 implementation으로 지정합니다(다른 모듈(ProductSwagger)의 패턴과 일치).
src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (2)

8-8: Command에 도메인 엔티티(Product) 직접 포함 — 경계 결합도 재검토 제안

애플리케이션 커맨드가 도메인 엔티티를 직접 들고 다니면 계층 간 결합도가 높아집니다. 장기적으로는 id/name/price 정도만 담는 경량 스냅샷(예: ProductSnapshot{id, name, price})을 전달하고, 도메인 엔티티는 도메인 계층 내부에서만 사용하도록 분리하는 것을 고려해보세요. 응답 렌더링과 가격 스냅샷 보존에도 유리합니다.


25-31: productList와 요청 아이템 간 불일치 사전 검증 추가 권장

상위 계층에서 이미 검증하더라도, 방어적으로 커맨드 생성 시 교차검증을 추가하면 버그를 조기 탐지할 수 있습니다.

다음과 같이 require 체크를 삽입하세요:

-        fun toCommand(userId: Long, productList: List<Product>, request: OrderCreateRequest): OrderCreateCommand {
-            return OrderCreateCommand(
+        fun toCommand(userId: Long, productList: List<Product>, request: OrderCreateRequest): OrderCreateCommand {
+            val reqIds = request.orderItems.map { it.productId }.toSet()
+            val productIds = productList.map { it.id }.toSet()
+            require(productIds.containsAll(reqIds)) {
+                "요청 항목에 포함된 상품 ID가 productList에 없습니다: ${reqIds - productIds}"
+            }
+            return OrderCreateCommand(
                 userId = userId,
                 productList = productList,
                 shippingAddress = request.shippingAddress,
                 items = request.orderItems.map { Item.of(it) },
             )
         }
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (2)

27-28: 사소한 가독성: 프로퍼티 참조 활용

associateBy { it.id } 대신 프로퍼티 참조를 사용하면 더 간결합니다.

-            val productMapByIds = productList.associateBy { it.id }
+            val productMapByIds = productList.associateBy(Product::id)

25-49: 결손 상품 시나리오 테스트 케이스 보강 제안

상품이 삭제/숨김되어 productList에 존재하지 않는 경우에도 주문 항목이 유지되고 폴백명이 노출되는지에 대한 단위/통합 테스트를 추가하면 회귀를 방지할 수 있습니다. 필요하시면 테스트 스켈레톤을 드릴 수 있습니다.

src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (1)

20-26: 상품 조회 최적화 및 명확화 제안 (선택적 리팩토링)

  • 요청 아이템에서 productId만 추출한 뒤 .distinct()를 적용해 중복 제거 → DB 조회 횟수 감소
  • 변수명을 productIds로 분리해 의도 명확화
  • 현재 주문 가격 결정은 도메인 OrderServicepriceAtPurchase = product.getPrice() 로 스냅샷 가격을 사용 중(확인 완료)하므로, 이 사실을 KDoc 또는 README 등 문서에 명시하면 TOCTOU 리스크 완화에 도움이 됩니다

예시 diff:

-        val productList = productQueryService.getProductsExists(request.orderItems.map { it.productId })
+        val productIds = request.orderItems.map { it.productId }.distinct()
+        val productList = productQueryService.getProductsExists(productIds)
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (1)

47-54: size 입력 상한 적용으로 API 안전성 강화 제안

컨트롤러 레벨에서도 상한을 적용하면 오입력/악의적 요청에 더 안전합니다. 서비스와 동일한 상한으로 맞춰 주세요.

-    ): SliceResponse<OrderSummary> {
-        val slice = orderFacade.getOrdersByCursor(userId, cursorId, size)
+    ): SliceResponse<OrderSummary> {
+        val limitedSize = size.coerceIn(1, MAX_PAGE_SIZE)
+        val slice = orderFacade.getOrdersByCursor(userId, cursorId, limitedSize)
         return SliceResponse.from(
             slice = slice,
             mapper = OrderSummary::from,
             cursorExtractor = { Cursor(it.id) }
         )
     }

파일 내(예: 클래스 하단)에 상수 추가:

private const val MAX_PAGE_SIZE = 50
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1e47da7 and 6e035f3.

📒 Files selected for processing (14)
  • src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderRepository.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/dto/OrderCreateRequest.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/dto/OrderSummary.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/swagger/OrderSwagger.kt (2 hunks)
  • src/test/kotlin/com/dh/baro/look/Fixture.kt (1 hunks)
  • src/test/kotlin/com/dh/baro/look/domain/LookReactionServiceTest.kt (1 hunks)
  • src/test/kotlin/com/dh/baro/look/domain/LookServiceTest.kt (1 hunks)
  • src/test/kotlin/com/dh/baro/product/Fixture.kt (1 hunks)
  • src/test/kotlin/com/dh/baro/product/domain/ProductQueryServiceTest.kt (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/test/kotlin/com/dh/baro/look/domain/LookReactionServiceTest.kt
  • src/test/kotlin/com/dh/baro/look/domain/LookServiceTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/kotlin/com/dh/baro/order/domain/OrderRepository.kt
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: codrin2
PR: S-BARO/server#17
File: src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt:38-59
Timestamp: 2025-08-12T13:52:29.449Z
Learning: codrin2 prefers direct field access for immutable id properties (val) rather than using getters, reasoning that immutable fields pose less encapsulation risk.
🧬 Code graph analysis (11)
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderCreateRequest.kt (2)
src/main/kotlin/com/dh/baro/order/presentation/OrderCreateRequest.kt (2)
  • message (8-25)
  • message (18-24)
src/main/kotlin/com/dh/baro/product/presentation/dto/ProductCreateRequest.kt (1)
  • min (7-46)
src/test/kotlin/com/dh/baro/product/domain/ProductQueryServiceTest.kt (1)
src/test/kotlin/com/dh/baro/product/domain/Fixture.kt (1)
  • productFixture (10-38)
src/test/kotlin/com/dh/baro/product/Fixture.kt (1)
src/test/kotlin/com/dh/baro/product/domain/Fixture.kt (2)
  • productFixture (10-38)
  • categoryFixture (7-8)
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (2)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/product/presentation/dto/ProductDetail.kt (1)
  • id (7-28)
src/main/kotlin/com/dh/baro/order/presentation/swagger/OrderSwagger.kt (3)
src/main/kotlin/com/dh/baro/product/presentation/swagger/ProductSwagger.kt (2)
  • summary (121-160)
  • summary (77-118)
src/main/kotlin/com/dh/baro/look/presentation/swagger/LookSwagger.kt (1)
  • summary (120-165)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • getOrdersByCursor (21-32)
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderSummary.kt (2)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderDetailResponse.kt (1)
  • orderId (8-41)
src/test/kotlin/com/dh/baro/look/Fixture.kt (2)
src/main/kotlin/com/dh/baro/look/domain/Look.kt (1)
  • name (8-125)
src/main/kotlin/com/dh/baro/look/domain/LookImage.kt (1)
  • name (7-40)
src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (1)
src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (3)
  • productRepository (11-79)
  • createOrder (18-26)
  • createOrderV2 (28-36)
src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (2)
src/test/kotlin/com/dh/baro/order/application/OrderQueryFacadeTest.kt (2)
  • classes (30-160)
  • orderQueryFacade (79-79)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (2)
  • readOnly (11-33)
  • getOrderDetail (17-19)
src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (4)
src/main/kotlin/com/dh/baro/product/application/ProductFacade.kt (1)
  • storeService (12-51)
src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (2)
  • productRepository (11-79)
  • createOrderV2 (28-36)
src/test/kotlin/com/dh/baro/order/application/OrderQueryFacadeTest.kt (2)
  • classes (30-160)
  • cmd (71-76)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2)
src/main/kotlin/com/dh/baro/product/presentation/ProductController.kt (1)
  • productFacade (15-76)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
🔇 Additional comments (14)
src/test/kotlin/com/dh/baro/look/Fixture.kt (2)

1-3: 패키지 이동 및 도메인 타입 import 정리, 방향성 좋습니다

  • 테스트에서 com.dh.baro.look.lookFixture로 가져다 쓰기 쉬워졌고, Look는 명시적 import로 의존 계층이 명확해졌습니다.
  • 과거 러닝(직접 필드 접근 선호)과도 충돌이 없습니다.

1-3: 잔존 Import 경로 없음 확인 완료

  • [1/2] 스크립트 실행 결과, com.dh.baro.look.domain.lookFixture 또는 FixtureKt 경로로의 import가 더 이상 발견되지 않았습니다.
  • [2/2] 테스트 코드(LookReactionServiceTest.kt, LookServiceTest.kt)에서는 새 패키지 경로인 com.dh.baro.look.lookFixture로 정상적으로 import 되어 있습니다.

따라서 추가 수정 없이 머지하셔도 됩니다.

src/test/kotlin/com/dh/baro/product/domain/ProductQueryServiceTest.kt (2)

4-4: 픽스처 패키지 정리 반영 👍

도메인 테스트에서 product 모듈 하위의 categoryFixture를 가져오도록 변경되어 테스트 간 결합도가 낮아졌습니다.


8-8: 픽스처 import 일관화 👍

productFixture의 import 경로가 통합되어, 테스트 간 재사용성이 좋아졌습니다.

src/test/kotlin/com/dh/baro/product/Fixture.kt (1)

1-4: 테스트 픽스처 패키지/의존성 정리 👍

패키지를 com.dh.baro.product로 승격하고 도메인 타입을 명시 import 하여, 다양한 도메인 테스트에서 재사용하기 쉬워졌습니다.

src/main/kotlin/com/dh/baro/order/presentation/dto/OrderCreateRequest.kt (2)

9-16: 요청 DTO의 유효성 검증 구성이 적절합니다

  • shippingAddress: NotBlank + Size(max=500) 조합이 도메인 제약(orders.shipping_address length 500)과 일치합니다.
  • orderItems: @Valid로 중첩 항목까지 검증되며, 각 항목의 productId/quantity에 Positive 적용이 적합합니다.

Also applies to: 18-24


1-1: 임포트 경로 변경 정상 적용됨
rg 검사를 통해 과거 패키지(com.dh.baro.order.presentation.OrderCreateRequest)를 참조한 부분은 없으며, 컨트롤러, 스웨거, 애플리케이션, 커맨드 등 모든 관련 클래스에서 신규 패키지(com.dh.baro.order.presentation.dto.OrderCreateRequest)로 임포트가 올바르게 반영되었음을 확인했습니다.

src/main/kotlin/com/dh/baro/order/presentation/swagger/OrderSwagger.kt (2)

5-7: 와일드카드 대신 명시적 DTO import로 변경한 점 좋습니다

API 표면을 정의하는 스웨거 인터페이스에서 명시적 import는 가독성과 변경 추적에 유리합니다.


173-173: OrderListItem 레퍼런스 모두 제거 확인 완료
SliceResponse<OrderSummary> 전환 이후 코드베이스 내에 OrderListItem 참조가 전혀 남아있지 않음을 확인했습니다. 추가 검토나 수정이 필요하지 않습니다.

src/main/kotlin/com/dh/baro/order/presentation/dto/OrderSummary.kt (1)

16-21: 단순·명확한 매핑이며, 불변 id 직접 접근 선호와도 일치합니다

order.id/status/totalPrice/createdAt를 그대로 투영해 가독성이 좋습니다. 또한 과거 러닝스(2025-08-12)에서 선호하셨던 “불변 id는 getter 대신 필드 접근” 스타일과도 일치합니다.

src/main/kotlin/com/dh/baro/order/application/OrderCreateCommand.kt (1)

25-31: toCommand 시그니처 변경 및 OrderService 조회 로직 검증 완료

  • OrderCreateCommand.toCommand(userId, productList, request) 시그니처가 정상 변경되었으며, OrderFacade에서도 올바르게 호출되고 있습니다.
  • OrderService 내에 더 이상 productRepository.findById 또는 findByIdForUpdate 호출이 없고, cmd.productList.associateBy { it.id }를 통해 제품 정보를 가져오도록 리팩토링되었습니다.
  • 남아 있는 productRepository.deductStock(item.productId, item.quantity) 호출은 재고 차감 책임을 repository 계층에 위임하는 의도된 흐름으로 보입니다.

위 사항을 종합할 때, 별도의 추가 변경 없이 현재 로직이 의도대로 동작하도록 잘 반영된 것으로 판단됩니다.

src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (1)

46-46: 응답 항목 정렬 기준 명시로 재현성 확보

productId 기준 정렬로 응답 재현성을 보장한 점 좋습니다(클라이언트 단 캐싱/디프에도 유리).

src/main/kotlin/com/dh/baro/order/domain/OrderQueryService.kt (1)

11-18: OrderQueryService 조회 변경: 인덱스 설정 검증 필요

메서드 시그니처(orderId → userId)와 호출부 인자 순서는 적절히 반영되어 있습니다. 다만 성능과 안정성을 위해 DB에 복합 인덱스 (user_id, id) 또는 (id, user_id)가 실제로 존재하는지 반드시 확인하거나, 없을 경우 추가 작업이 필요합니다.

– src/main/kotlin/com/dh/baro/order/domain/OrderRepository.kt
fun findByIdAndUserId(orderId: Long, userId: Long): Order? 정의 확인
– src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt
orderQueryService.getOrderDetailByUserId(orderId, userId) 호출부 인자 순서 일치 확인
– src/main/kotlin/com/dh/baro/order/domain/Order.kt
@Table(name = "orders")indexes = [Index(columnList = "user_id, id")] 등 인덱스 설정 추가 검토 또는
• 별도 Migration(SQL) 파일에 CREATE INDEX … ON orders(user_id, id); 추가 검토

위 사항을 반영해 DB 스키마와 애플리케이션 간 호출·조회 일관성을 보장해 주세요.

src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (1)

27-29: Bundle → DTO 매핑 전환 깔끔합니다.

OrderDetailBundle을 컨트롤러에서 DTO로 변환하는 책임이 명확해졌습니다. 유지하세요.

Comment on lines 28 to 32
fun getOrderDetail(userId: Long, orderId: Long): OrderDetailBundle {
val order = orderQueryService.getOrderDetailByUserId(orderId, userId)
val productList = productQueryService.getProductsExists(order.items.map { orderItem -> orderItem.productId })
return OrderDetailBundle(order, productList)
}
Copy link

@coderabbitai coderabbitai bot Aug 26, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

삭제/비공개 상품이 있는 과거 주문 상세 조회가 실패할 수 있습니다. 부분 부재 허용으로 전환하세요.

getProductsExists(...)가 일부 productId 미존재 시 예외를 던진다면, 과거 주문의 상품이 삭제된 경우 주문 상세 전체가 실패할 수 있습니다. 주문 레코드는 여전히 존재하므로 상세는 반드시 열려야 합니다. 프레젠테이션 레이어는 priceAtPurchase 등 주문 아이템에 저장된 스냅샷 값으로 렌더링하고, 상품명이 필요한 경우에만 조회된 상품 리스트를 보조적으로 사용하세요.

다음과 같이 최소한 중복 제거는 적용하고, 가능하면 “관용적(관대한)” 조회 메서드로 교체하는 것을 권장합니다.

-        val productList = productQueryService.getProductsExists(order.items.map { orderItem -> orderItem.productId })
+        val productIds = order.items.map { it.productId }.distinct()
+        // 존재하지 않는 상품이 있어도 예외 없이 가능한 것만 반환하는 메서드 사용 권장
+        val productList = productQueryService.getProductsByIds(productIds) // 존재하지 않으면 제외
         return OrderDetailBundle(order, productList)

만약 getProductsByIds가 없다면, 임시로 예외를 흡수하고 빈/부분 리스트로 대체하는 방식을 고려해 주세요:

val productIds = order.items.map { it.productId }.distinct()
val productList = runCatching {
    productQueryService.getProductsExists(productIds)
}.getOrElse { emptyList() } // 존재하지 않는 상품이 있어도 주문 상세는 열리게

추가로, OrderDetailResponse.from(order, productList)가 상품 부재 시에도 priceAtPurchase 등 아이템 보존 필드로 안전하게 렌더링하는지 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# tolerant 렌더링 경로 확인: 아이템 보존 필드 사용 여부 탐색
rg -nP 'OrderDetailResponse\.from\s*\(' --type=kt -C3
rg -nP 'priceAtPurchase|item\.price|snapshot|purchased' --type=kt -C3

Length of output: 181


🏁 Script executed:

#!/bin/bash
# OrderDetailResponse.from 호출부 및 주변 로직 확인
rg -n 'OrderDetailResponse\.from' -C3 -g '*.kt'

# priceAtPurchase, snapshot, purchased, item.price 사용 여부 탐색
rg -n 'priceAtPurchase|snapshot|purchased|item\.price' -C3 -g '*.kt'

# productQueryService.getProductsByIds 메서드 존재 여부 확인
rg -n 'getProductsByIds' -C3 -g '*.kt'

Length of output: 7678


추가 검증을 위해 OrderDetailResponse.from 구현 내용과 getProductsExists 정의 및 ProductQueryService 인터페이스/구현부를 확인해 보겠습니다.


🏁 Script executed:

#!/bin/bash
# OrderDetailResponse.from 전체 구현 확인
sed -n '1,200p' src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt

# getProductsExists 메서드 정의 위치 탐색
rg -n 'fun getProductsExists' -C5 -g '*.kt'

# ProductQueryService 인터페이스 및 구현부 확인
rg -n 'interface ProductQueryService' -C5 -g '*.kt'
rg -n 'class .*ProductQueryServiceImpl' -C5 -g '*.kt'

Length of output: 3157


강제 적용: 삭제/비공개 상품에 대한 주문 상세 내구성 확보

이 커밋 기준으로:

  • productQueryService.getProductsExists는 일부 상품 미존재 시 예외를 던져 주문 상세 전체가 실패합니다.
  • OrderDetailResponse.fromproductList에 없는 상품(item)을 전부 필터링해 결과에서 누락시킵니다.

따라서 삭제 또는 비공개된 상품이 포함된 과거 주문은 상세 조회가 불가능하거나 잘못 표시될 수 있습니다. 다음 사항을 필수로 리팩터링하세요.

  1. ProductQueryService에 “관대한 조회” 메서드 추가

    @Service
    class ProductQueryService(
        private val productRepository: ProductRepository,
    ) {
        // 기존 getProductsExists 유지
        // 신규: 미존재 상품은 제외하고 반환
        @Transactional(readOnly = true)
        fun getProductsByIds(productIds: List<Long>): List<Product> {
            return productRepository.findAllById(productIds.toSet()).toList()
        }
    }
  2. OrderFacade.getOrderDetail 수정 (중복 제거 + 관용적 조회 메서드 사용)

     fun getOrderDetail(userId: Long, orderId: Long): OrderDetailBundle {
         val order = orderQueryService.getOrderDetailByUserId(orderId, userId)
    -    val productList = productQueryService.getProductsExists(order.items.map { it.productId })
    +    val productIds   = order.items.map { it.productId }.distinct()
    +    val productList  = productQueryService.getProductsByIds(productIds)
         return OrderDetailBundle(order, productList)
     }
  3. OrderDetailResponse.from 매핑 로직 강화

    • mapNotNull 대신 map을 사용해 모든 주문 아이템을 유지하고, 상품 정보가 없을 경우 기본값(예: “삭제된 상품”)으로 대체
     companion object {
         fun from(order: Order, productList: List<Product>): OrderDetailResponse {
             val productMapByIds = productList.associateBy { it.id }
    
             return OrderDetailResponse(
                 orderId       = order.id,
                 orderStatus   = order.status,
                 shippingAddress = order.shippingAddress,
                 totalPrice    = order.totalPrice,
                 orderedAt     = order.createdAt,
    -            items = order.items
    -                .mapNotNull { item ->
    -                    productMapByIds[item.productId]?.let { product ->
    -                        Item(
    -                            productId       = item.productId,
    -                            productName     = product.getName(),
    -                            quantity        = item.quantity,
    -                            priceAtPurchase = item.priceAtPurchase,
    -                        )
    -                    }
    -                }
    +            items = order.items
    +                .map { item ->
    +                    val product = productMapByIds[item.productId]
    +                    Item(
    +                        productId       = item.productId,
    +                        productName     = product?.getName() ?: "삭제된 상품",
    +                        quantity        = item.quantity,
    +                        priceAtPurchase = item.priceAtPurchase,
    +                    )
    +                }
             )
         }
     }

위 변경으로, 삭제/비공개 상품이 포함된 과거 주문도 주문 상세가 항상 정상 렌더링되며, priceAtPurchase 등 저장된 스냅샷 필드를 활용해 누락 없이 표시됩니다.

🤖 Prompt for AI Agents
In src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt around lines
28-32, the current call to productQueryService.getProductsExists causes
exceptions when some products are deleted/private and OrderDetailResponse.from
filters out missing items; change to use a tolerant product lookup and preserve
all order items: add a new ProductQueryService.getProductsByIds(productIds:
List<Long>) that returns existing products without throwing, update
OrderFacade.getOrderDetail to call that method (deduplicate IDs before querying)
and pass the result through, and update OrderDetailResponse.from to map over all
order.items (not mapNotNull), replacing missing product info with a placeholder
"deleted/hidden product" representation and using stored snapshot fields like
priceAtPurchase to render item details so past orders always render completely.

Copy link
Member Author

Choose a reason for hiding this comment

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

OrderItem에 Product_name 과 thumnail_url 필드를 추가해서 상품 정보를 보존하는 방식으로 변경했습니다.

Copy link

Choose a reason for hiding this comment

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

@codrin2 Perfect! That's actually a much better solution than what I suggested.

By adding product_name and thumbnail_url fields directly to OrderItem, you've implemented proper snapshot preservation - storing the product state at the time of purchase. This approach:

  1. Eliminates the dependency on product lookups entirely during order detail retrieval
  2. Guarantees data integrity - past orders will always display correctly regardless of product changes/deletions
  3. Improves performance - no additional database queries needed for product information
  4. Follows e-commerce best practices - preserving historical transaction data

This denormalization strategy is the standard approach for order systems where you need to maintain an immutable record of what was actually purchased. Much cleaner than the workaround I proposed!


✏️ Learnings added
Learnt from: codrin2
PR: S-BARO/server#28
File: src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt:28-32
Timestamp: 2025-08-26T01:43:31.936Z
Learning: In e-commerce order systems, OrderItem entities should store snapshot fields like product_name and thumbnail_url at purchase time to preserve product information independently of the current Product state. This denormalization approach ensures past orders remain viewable even if products are deleted or modified, and eliminates the need for product lookups during order detail retrieval.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: codrin2
PR: S-BARO/server#17
File: src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt:38-59
Timestamp: 2025-08-12T13:52:29.449Z
Learning: codrin2 prefers direct field access for immutable id properties (val) rather than using getters, reasoning that immutable fields pose less encapsulation risk.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

- 주문 당시의 상품명(name)과 썸네일(thumbnailUrl)을 OrderItem에 저장하도록 구조 확장
- 주문 생성 시점의 상품 정보를 보존함으로써 이후 상품명/썸네일이 변경되더라도 과거 주문 내역이 영향받지 않도록 개선
- 데이터 정합성 및 주문 내역 안정성 확보 목적의 확장 설계
@S-BARO S-BARO deleted a comment from coderabbitai bot Aug 26, 2025
@S-BARO S-BARO deleted a comment from coderabbitai bot Aug 26, 2025
@S-BARO S-BARO deleted a comment from coderabbitai bot Aug 26, 2025
Copy link

@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: 0

♻️ Duplicate comments (1)
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (1)

43-53: 페이지 크기 상한 일관성 검증(Controller → Facade → Service)

요청 size를 그대로 위임합니다. 서비스에서 상한을 강제한다면(예: coerceAtMost(MAX_PAGE_SIZE)), 세 레이어가 동일한 상한을 공유하는지 통합 테스트 한 케이스로 보강해 주세요.

#!/bin/bash
# size 상한 적용 여부 탐색
rg -nP '\bMAX_PAGE_SIZE\b|coerceAtMost' -C2 --type=kt
# getOrdersByCursor 시그니처·호출 일관성 점검
rg -nP '\bgetOrdersByCursor\s*\(' -C2 src/main/kotlin
🧹 Nitpick comments (4)
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (1)

21-29: 컨트롤러 단순화와 파사드 단일 의존성 전환 👍

OrderQueryFacade 의존 제거, OrderFacade 단일 진입으로 응집도가 개선되었습니다. 201 Created 반환도 적절합니다. 필요 시 Location 헤더(/orders/{id}) 추가를 고려해 보세요(선택).

src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (2)

45-47: require 안에서 예외를 던지는 패턴은 혼란스럽습니다 → 명시적 if로 변경 권장

require(updated != 0) { throw ConflictException(...) }는 동작하긴 하지만 가독성이 떨어집니다. 아래처럼 명시적 분기 처리가 더 명확합니다.

-        require(updated != 0) {
-            throw ConflictException(ErrorMessage.OUT_OF_STOCK.format(item.productId))
-        }
+        if (updated == 0) {
+            throw ConflictException(ErrorMessage.OUT_OF_STOCK.format(item.productId))
+        }

49-56: OrderItem 생성 시 스냅샷 값 주입 흐름 적합

name, thumbnailUrl, priceAtPurchaseProduct에서 읽어 스냅샷으로 저장하는 구현이 의도와 일치합니다. 다만 Product의 가격 스케일이 0이 아닐 가능성에 대비해(환경에 따라) 필요 시 setScale(0, RoundingMode.HALF_UP)로 정규화하는 것을 고려하세요(선택).

src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (1)

21-24: 중복 productId 제거로 불필요한 조회 줄이기

같은 상품이 여러 번 요청될 수 있으므로 조회 전 distinct()를 적용하면 DB/캐시 부하를 줄일 수 있습니다.

-        val productList = productQueryService.getProductsExists(request.orderItems.map { orderItem -> orderItem.productId })
+        val productIds = request.orderItems.map { it.productId }.distinct()
+        val productList = productQueryService.getProductsExists(productIds)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e035f3 and 9e09a15.

📒 Files selected for processing (6)
  • src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (3 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (1 hunks)
  • src/main/kotlin/com/dh/baro/order/presentation/swagger/OrderSwagger.kt (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/kotlin/com/dh/baro/order/presentation/swagger/OrderSwagger.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: codrin2
PR: S-BARO/server#17
File: src/main/kotlin/com/dh/baro/look/presentation/dto/LookDetailResponse.kt:38-59
Timestamp: 2025-08-12T13:52:29.449Z
Learning: codrin2 prefers direct field access for immutable id properties (val) rather than using getters, reasoning that immutable fields pose less encapsulation risk.
📚 Learning: 2025-08-26T01:43:31.936Z
Learnt from: codrin2
PR: S-BARO/server#28
File: src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt:28-32
Timestamp: 2025-08-26T01:43:31.936Z
Learning: In e-commerce order systems, OrderItem entities should store snapshot fields like product_name and thumbnail_url at purchase time to preserve product information independently of the current Product state. This denormalization approach ensures past orders remain viewable even if products are deleted or modified, and eliminates the need for product lookups during order detail retrieval.

Applied to files:

  • src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt
  • src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt
  • src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt
🧬 Code graph analysis (5)
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (2)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderListItem.kt (1)
  • orderId (8-23)
src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (3)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderDetailResponse.kt (1)
  • productId (17-22)
src/main/kotlin/com/dh/baro/product/domain/Product.kt (1)
  • name (11-135)
src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (2)
src/test/kotlin/com/dh/baro/order/domain/OrderServiceTest.kt (2)
  • classes (25-173)
  • order (143-148)
src/main/kotlin/com/dh/baro/order/domain/Order.kt (1)
  • name (10-65)
src/main/kotlin/com/dh/baro/order/presentation/OrderController.kt (2)
src/test/kotlin/com/dh/baro/order/application/OrderQueryFacadeTest.kt (1)
  • classes (30-160)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (3)
src/main/kotlin/com/dh/baro/product/application/ProductFacade.kt (1)
  • storeService (12-51)
src/test/kotlin/com/dh/baro/order/application/OrderQueryFacadeTest.kt (2)
  • classes (30-160)
  • cmd (71-76)
src/main/kotlin/com/dh/baro/order/application/OrderQueryFacade.kt (1)
  • readOnly (11-33)
🔇 Additional comments (8)
src/main/kotlin/com/dh/baro/order/presentation/dto/OrderDetailResponse.kt (2)

25-44: 스냅샷 필드 기반 매핑으로 과거 주문 복원성 확보 — 아주 좋습니다

from(order)에서 OrderItemname, thumbnailUrl, priceAtPurchase 스냅샷을 그대로 사용해 응답을 구성합니다. 이로써 상품 삭제/변경과 무관하게 과거 주문 상세가 안정적으로 렌더링됩니다. 이전 커밋에서 지적된 productList 조인(mapNotNull)으로 인한 항목 누락 이슈도 구조적으로 해소되었습니다.


33-44: 정렬 기준 재확인: productId 정렬이 UX 기대와 일치하는지 검증 필요

현재 항목은 productId로 정렬합니다. 다만 주문서 상의 원래 추가 순서를 유지하는 편이 사용자 기대에 더 부합할 수 있습니다(특히 같은 주문에서 상품 ID가 뒤섞여 보일 수 있음). Order.itemsmutableSetOf()로 생성되어 LinkedHashSet 삽입 순서가 보존된다면 정렬 없이 그대로 내보내는 것도 대안입니다. 팀의 UX 의도에 맞는지를 확인해 주세요.

src/main/kotlin/com/dh/baro/order/domain/OrderItem.kt (2)

19-27: Product 연관 제거 + 스냅샷 칼럼 도입 방향 적합

product_id, product_name, thumbnail_url를 명시적으로 보관하도록 리모델링한 선택이 훌륭합니다. 조회 시 추가 조인이 필요 없고, 과거 주문의 독립성이 보장됩니다.


41-57: 도메인 불변식 검증 추가 제안

생성 팩토리 newOrderItemrequire 구문을 넣어 수량·가격·이름·썸네일 검증을 수행하면
DB 제약 위반 시점에 비즈니스 예외로 조기에 처리할 수 있습니다. 예시는 다음과 같습니다.

fun newOrderItem(
    order: Order,
    productId: Long,
    name: String,
    thumbnailUrl: String,
    quantity: Int,
    priceAtPurchase: BigDecimal,
): OrderItem = run {
    require(quantity > 0) { "quantity must be > 0" }
    require(priceAtPurchase >= BigDecimal.ZERO) { "priceAtPurchase must be >= 0" }
    require(name.isNotBlank()) { "product name must not be blank" }
    require(thumbnailUrl.isNotBlank()) { "thumbnailUrl must not be blank" }
    OrderItem(
        id = IdGenerator.generate(),
        order = order,
        productId = productId,
        name = name,
        thumbnailUrl = thumbnailUrl,
        quantity = quantity,
        priceAtPurchase = priceAtPurchase,
    )
}

추가로, OrderItem.namethumbnailUrl 필드에 지정된 길이 제한(예: 100/300)이
Product 엔티티의 @Column(length=...) 설정과 일치하는지 수동으로 확인해 주세요.
불일치할 경우, 잘림(truncation) 또는 DB 예외가 발생할 수 있습니다.

  • 확인 대상 컬럼:
    • Product.product_name(@Column(name="product_name", length = ?))
    • Product.thumbnail_url(@Column(name="thumbnail_url", length = ?))
src/main/kotlin/com/dh/baro/order/domain/OrderService.kt (2)

18-27: 제품 목록 주입으로 내부 조회 제거 — 성능/응집도 개선

cmd.productListassociateBy하여 재조회 없이 아이템 구성하는 방향이 좋습니다. 트랜잭션 내 저장 전 updateTotalPrice() 호출도 타이밍이 적절합니다.


29-37: 중복 아이템 병합 로직 OK, 다만 입력 유효성은 상위 레이어에서 보장 필요

groupBy + sumOf로 동일 상품 수량을 합산합니다. 컨트롤러 DTO에 @Size(min=1) 및 각 아이템에 @Min(1) 등의 Bean Validation이 적용되어 있는지 확인해 주세요(없다면 추가 권장).

src/main/kotlin/com/dh/baro/order/application/OrderFacade.kt (2)

20-26: 주문 생성 플로우의 역할 분리와 검증 순서 적절

checkUserExists → getProductsExists → toCommand → createOrder 순으로 책임이 깔끔히 분배되어 있습니다.


28-42: 상세/커서 조회를 QueryService로 위임하는 구조 적절

파사드가 읽기 경로를 OrderQueryService로 위임하여 트랜잭션 경계를 단순화했습니다. 컨트롤러 단에서 DTO 변환을 수행하므로 파사드의 책임도 명확합니다.

@codrin2 codrin2 merged commit c615c8b into develop Aug 26, 2025
2 checks passed
@codrin2 codrin2 deleted the refactor/#27 branch August 26, 2025 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] OrderItem과 Product 연관 제거 및 주문 흐름/응답/테스트 전면 개선

2 participants