[volume-3] 도메인 & 객체 설계 및 아키텍처, 패키지 구성#129
[volume-3] 도메인 & 객체 설계 및 아키텍처, 패키지 구성#129MINJOOOONG wants to merge 16 commits intoLoopers-dev-lab:mainfrom
Conversation
📝 WalkthroughWalkthrough사용자 좋아요(Like)와 주문(Order) 관련 도메인·서비스·퍼사드·JPA 인프라·REST 컨트롤러·DTO 및 관련 테스트를 신규 추가한다. 재고·포인트·상품 존재 검증 흐름과 좋아요 생성/취소/조회 로직이 포함된다. Changes
Sequence DiagramssequenceDiagram
participant Client as 클라이언트
participant Controller as LikeV1Controller
participant Facade as LikeFacade
participant Service as LikeService
participant ProductRepo as ProductRepository
participant LikeRepo as LikeRepository
participant DB as 데이터베이스
Client->>Controller: POST /api/v1/products/{productId}/likes (X-Loopers-UserId)
Controller->>Facade: like(userId, productId)
Facade->>ProductRepo: existsById(productId)
ProductRepo->>DB: SELECT EXISTS / SELECT by id
DB-->>ProductRepo: 존재/비존재
alt 상품 없음
Facade-->>Controller: CoreException(NOT_FOUND)
Controller-->>Client: ApiResponse 오류
else 상품 있음
Facade->>Service: like(userId, productId)
Service->>LikeRepo: findByUserIdAndProductId(userId, productId)
LikeRepo->>DB: SELECT FROM likes
DB-->>LikeRepo: Optional<Like>
alt 좋아요 없음
Service->>LikeRepo: save(new Like(...))
LikeRepo->>DB: INSERT INTO likes
DB-->>LikeRepo: 저장된 Like
else 이미 좋아요
Service-->>Service: 기존 Like 반환
end
Service-->>Facade: Like
Facade-->>Controller: LikeInfo -> LikeResponse
Controller-->>Client: ApiResponse.success(LikeResponse)
end
sequenceDiagram
participant Client as 클라이언트
participant Controller as OrderV1Controller
participant Facade as OrderFacade
participant UserRepo as UserRepository
participant ProductRepo as ProductRepository
participant Service as OrderService
participant OrderRepo as OrderRepository
participant DB as 데이터베이스
Client->>Controller: POST /api/v1/orders (X-Loopers-UserId, CreateRequest)
Controller->>Facade: createOrder(userId, itemRequests)
Facade->>UserRepo: findById(userId)
UserRepo->>DB: SELECT * FROM users
DB-->>UserRepo: Optional<User>
alt 사용자 없음
Facade-->>Controller: CoreException(NOT_FOUND)
Controller-->>Client: ApiResponse 오류
else 사용자 있음
loop 각 주문 항목
Facade->>ProductRepo: existsById(productId)
ProductRepo->>DB: SELECT EXISTS
DB-->>ProductRepo: 존재/비존재
alt 상품 없음
Facade-->>Controller: CoreException(NOT_FOUND) and abort
else 상품 있음
Facade->>ProductRepo: findById(productId)
ProductRepo->>DB: SELECT * FROM products
DB-->>ProductRepo: Product
Facade->>Facade: product.decreaseStock(quantity)
Facade->>Facade: OrderItem 생성
end
end
Facade->>Facade: Order 생성 및 user.deductPoint(totalPrice)
Facade->>Service: createOrder(order)
Service->>OrderRepo: save(order)
OrderRepo->>DB: INSERT INTO orders
DB-->>OrderRepo: 저장된 Order (id)
loop 각 주문 항목
Service->>OrderRepo: saveItem(orderItem)
OrderRepo->>DB: INSERT INTO order_items
DB-->>OrderRepo: 저장된 OrderItem
end
Service-->>Facade: 저장된 Order
Facade-->>Controller: OrderInfo -> OrderResponse
Controller-->>Client: ApiResponse.success(OrderResponse)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분 Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (7)
apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceUnitTest.java-42-54 (1)
42-54:⚠️ Potential issue | 🟡 Minor정규화 기반 중복 등록 케이스가 빠져 있어 중복 방지 회귀를 탐지하기 어렵다.
Line 42~54은 동일 문자열 중복만 검증한다. 운영에서는 공백 포함 입력이 빈번하므로
" 나이키 "선등록 후"나이키"등록 시CONFLICT를 검증하는 테스트를 추가해야 한다. 이 케이스가 없으면 trim 정책 변경 시 중복 데이터 유입을 놓칠 수 있다.
As per coding guidelines "**/*Test*.java: 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceUnitTest.java` around lines 42 - 54, Add a new unit test in BrandServiceUnitTest (alongside throwsException_whenNameAlreadyExists) that verifies normalization/trim-based deduplication: first call brandService.register(" 나이키 ") then assert that brandService.register("나이키") throws CoreException with ErrorType.CONFLICT; ensure the test uses the same assertThrows pattern and checks result.getErrorType() to guard against regressions if trimming behavior changes.apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandNameUnitTest.java-18-53 (1)
18-53:⚠️ Potential issue | 🟡 Minor경계값(정확히 50자)과 정규화 결과 검증이 누락되어 회귀를 놓칠 수 있다.
Line 18~53은 정상/실패 케이스를 잘 다루지만, 운영 관점에서 가장 자주 깨지는 경계값(정확히 50자 허용)과 trim 정규화 결과(
" 나이키 "→"나이키") 검증이 없다.createsBrandName_whenValueIsExactly50Chars,createsBrandName_whenValueHasSurroundingSpaces테스트를 추가해 규칙 회귀를 차단해야 한다.
As per coding guidelines "**/*Test*.java: 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandNameUnitTest.java` around lines 18 - 53, Add two unit tests to BrandNameUnitTest: createsBrandName_whenValueIsExactly50Chars and createsBrandName_whenValueHasSurroundingSpaces. For createsBrandName_whenValueIsExactly50Chars, construct BrandName with a 50-char string (e.g., "a".repeat(50)) and assert no exception is thrown and brandName.value() equals that 50-char string. For createsBrandName_whenValueHasSurroundingSpaces, construct BrandName with a value padded with leading/trailing spaces (e.g., " 나이키 ") and assert creation succeeds and brandName.value() equals the trimmed string ("나이키"). Use the BrandName constructor and value() accessor and assert behavior matches existing tests' pattern (assertThrows/assertThat).apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java-14-15 (1)
14-15:⚠️ Potential issue | 🟡 Minor
orderId초기화 순서는 현재 코드에서 안전하지만, 엔티티 설계에 방어 로직이 부족하다.
OrderService.createOrder()에서는Order저장 →assignOrderId()호출 →OrderItem저장 순서로 진행되므로,saveItem()호출 시점에는 이미orderId가 할당된다. 따라서 현재 로직에서는nullable=false제약조건 위반이 발생하지 않는다.다만, 설계적 결함이 있다:
BaseEntity의guard()메서드는@PrePersist에서 호출되지만,OrderItem이 이를 오버라이드하지 않아orderId == null상태로 저장될 가능성이 있다. 다른 곳에서saveItem()이 호출되면 DB 에러로 터질 수 있다.해결책:
OrderItem에서guard()오버라이드하여orderId유효성 검증을 추가하거나,orderId를 생성자 파라미터로 받도록 설계 변경이 필요하다.`@Override` protected void guard() { if (this.orderId == null) { throw new CoreException(ErrorType.INTERNAL_ERROR, "orderId가 할당되지 않았습니다"); } }이렇게 하면 엔티티 수준에서 불변성을 보장하고, 부주의한 사용으로 인한 DB 제약조건 위반을 사전에 방지할 수 있다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java` around lines 14 - 15, OrderItem lacks entity-level validation for orderId which can lead to null being persisted; override BaseEntity.guard() in OrderItem to validate orderId (throw a CoreException/ErrorType.INTERNAL_ERROR with a clear message if null) or alternatively change OrderItem to require orderId in its constructor to ensure it's always set; locate the OrderItem class and implement the guard() override (or constructor change) and update usages like OrderService.createOrder(), assignOrderId(), and saveItem() to match the chosen approach.apps/commerce-api/src/test/java/com/loopers/domain/product/ProductUnitTest.java-75-91 (1)
75-91:⚠️ Potential issue | 🟡 MinorIncreaseStock 테스트에서 실패 케이스가 누락되었다.
DecreaseStock은 경계값과 실패 케이스를 모두 테스트하나,IncreaseStock은 성공 케이스만 존재한다.quantity < 1입력 시 예외 발생 테스트가 누락되어 프로덕션 코드의 검증 로직이 커버되지 않는다.💚 추가 테스트 케이스
`@DisplayName`("요청 수량이 1 미만이면, BAD_REQUEST 예외가 발생한다.") `@Test` void throwsException_whenQuantityIsLessThanOne() { // arrange Product product = new Product(1L, "상품A", 10000L, "설명", 5); // act & assert CoreException zeroResult = assertThrows(CoreException.class, () -> product.increaseStock(0)); assertThat(zeroResult.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); CoreException negativeResult = assertThrows(CoreException.class, () -> product.increaseStock(-1)); assertThat(negativeResult.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }As per coding guidelines: "단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/product/ProductUnitTest.java` around lines 75 - 91, Add a failing-case unit test to ProductUnitTest under the IncreaseStock nested class to cover validation when product.increaseStock is called with quantity < 1: create a Product instance and assert that calling product.increaseStock(0) and product.increaseStock(-1) throws CoreException with ErrorType.BAD_REQUEST (use assertThrows for both cases and assert the exception's getErrorType equals ErrorType.BAD_REQUEST) so the validation logic in Product.increaseStock is covered.apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java-31-37 (1)
31-37:⚠️ Potential issue | 🟡 Minor생성자에서 필드 유효성 검증이 누락되었다.
User,Order,OrderItem등 다른 도메인 엔티티는 생성자에서 필수 필드를 검증하나,Product는brandId,name,price에 대한 검증이 없다. 운영 환경에서 잘못된 데이터가 저장될 경우 디버깅이 어려워진다.🛠️ 수정안: 생성자 유효성 검증 추가
public Product(Long brandId, String name, Long price, String description, Integer stockQuantity) { + if (brandId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "브랜드 ID는 필수입니다"); + } + if (name == null || name.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 이름은 필수입니다"); + } + if (price == null || price < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 가격은 0 이상이어야 합니다"); + } this.brandId = brandId; this.name = name; this.price = price; this.description = description; this.stockQuantity = new StockQuantity(stockQuantity); }추가 테스트: 생성자 검증 로직에 대한 단위 테스트 추가 필요 (null brandId, blank name, negative price 케이스).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java` around lines 31 - 37, Add validation to the Product constructor (the Product(Long brandId, String name, Long price, String description, Integer stockQuantity) method) to guard required fields: require non-null brandId, non-blank name, and non-null non-negative price; throw an IllegalArgumentException (or use Objects.requireNonNull for null checks) with clear messages when violated, and keep existing StockQuantity creation for stockQuantity. Also add unit tests covering null brandId, blank name, and negative price to assert the constructor rejects invalid input.apps/commerce-api/src/test/java/com/loopers/application/order/FakeProductRepository.java-15-19 (1)
15-19:⚠️ Potential issue | 🟡 MinoraddProduct 메서드에서 생성된 Product의 ID가 store 키와 동기화되지 않는다.
sequence를 키로 사용하지만,Product객체 내부의 ID(BaseEntity에서 관리)는 설정되지 않는다. 테스트에서product.getId()를 호출하면null이 반환될 수 있어 프로덕션과 동작이 다를 수 있다.addProductWithId처럼 명시적 ID 매핑만 사용하거나, 반환된 Product를 store 키와 연결하는 방식을 검토해야 한다.🔧 개선 제안
addProduct메서드 사용을 지양하고, 테스트에서는addProductWithId를 사용하여 ID를 명시적으로 지정하는 것을 권장한다.- public Product addProduct(Long brandId, String name, Long price, String description, Integer stock) { - Product product = new Product(brandId, name, price, description, stock); - store.put(sequence++, product); - return product; - } + // 테스트에서는 addProductWithId를 사용하여 ID를 명시적으로 지정할 것을 권장 + public Product addProduct(Long brandId, String name, Long price, String description, Integer stock) { + long id = sequence++; + Product product = new Product(brandId, name, price, description, stock); + store.put(id, product); + // 주의: product.getId()는 null이며, store 키(id)와 다름 + return product; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/application/order/FakeProductRepository.java` around lines 15 - 19, addProduct에서 생성한 Product의 내부 ID가 BaseEntity에 설정되지 않아 store의 키(sequence)와 불일치할 수 있으므로 addProduct에서 Product에 현재 sequence 값을 명시적으로 설정한 뒤 store.put(sequence++, product) 하거나(순증가 시점 주의), 테스트에서 일관성을 위해 addProductWithId를 사용하도록 코드/테스트를 변경하세요; 대상 식별자는 addProduct, sequence, store, BaseEntity, addProductWithId입니다.apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceUnitTest.java-41-56 (1)
41-56:⚠️ Potential issue | 🟡 Minor재좋아요 시 멱등성 검증이 약하다.
현재 검증은 필드 값만 확인하므로 동일 값의 중복 데이터가 저장되어도 테스트가 통과할 수 있어 운영에서 카운트 부정확 문제가 늦게 발견될 수 있다.
수정안으로 두 번째like호출 후 저장 건수가 1건인지(countByProductId또는 user+product 기준 조회)까지 단언해 멱등성을 명시적으로 보장하는 것이 좋다.
추가 테스트로 동일 사용자/상품 조합 반복 호출 N회 후에도 건수가 1건인 케이스를 포함하는 것이 좋다.As per coding guidelines "단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다."를 근거로 한 제안이다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceUnitTest.java` around lines 41 - 56, The test only asserts fields on the returned Like but not repository state, so update the test in LikeServiceUnitTest to assert idempotency by checking the persistent count after the second like: call the repository method (e.g., likeRepository.countByUserIdAndProductId(userId, productId) or likeRepository.countByProductId(productId)) and assert it equals 1; additionally add a new test that calls likeService.like(userId, productId) N times (e.g., loop 5 times) and then asserts the same repository count is 1 to prove repeated calls remain idempotent. Ensure you reference the existing likeService.like(...) flow and use the repository counting methods for the assertions.
🧹 Nitpick comments (14)
apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java (1)
15-20: 동일 값 객체를 중복 생성해 규칙 변경 시 불일치 위험이 있다.Line 15와 Line 19에서
BrandName을 이미 생성했는데 원시name으로Brand를 다시 생성해 검증/정규화 경로가 이중화된다.Brand(BrandName)생성자를 도입해 단일 정규화 결과만 전달하도록 바꾸면 운영 중 규칙 변경 시 데이터 불일치를 줄일 수 있다. 추가로 공백 포함 입력 등록 후 저장 값 정규화를 검증하는 테스트를 보강해야 한다.변경 예시
public class BrandService { @@ public Brand register(String name) { BrandName brandName = new BrandName(name); if (brandRepository.existsByName(brandName)) { throw new CoreException(ErrorType.CONFLICT, "이미 등록된 브랜드 이름입니다"); } - Brand brand = new Brand(name); + Brand brand = new Brand(brandName); return brandRepository.save(brand); } }// Brand.java (선언 추가) public Brand(BrandName name) { this.name = name; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java` around lines 15 - 20, BrandService currently constructs a BrandName for validation then reconstructs a Brand from the raw name, causing duplicated normalization logic; change the code to construct the Brand using the already-created BrandName (add a Brand(BrandName) constructor in Brand) and call brandRepository.save with that Brand instance so validation/normalization is single-path (update BrandService uses of new Brand(name) to new Brand(brandName) and adjust/extend tests to assert normalization when saving inputs with surrounding whitespace).apps/commerce-api/src/test/java/com/loopers/domain/order/FakeOrderRepository.java (1)
25-31: 내부 저장소를 직접 노출해 테스트 격리가 깨질 수 있다.운영 관점에서 테스트 신뢰도가 낮아지면 배포 전 회귀 탐지가 약해진다. 외부 코드가 반환 리스트를 수정해 저장소 상태를 오염시킬 수 있으므로 방어적 복사 또는 불변 뷰를 반환하는 것이 안전하다. 반환 컬렉션을 수정해도 내부 상태가 변하지 않는지 검증하는 테스트를 추가하는 것이 좋다.
As per coding guidelines: `**/*.java` 파일에서는 null 처리, 방어적 복사, 불변성을 점검해야 한다.수정 예시
public List<Order> getOrders() { - return orders; + return List.copyOf(orders); } public List<OrderItem> getItems() { - return items; + return List.copyOf(items); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/order/FakeOrderRepository.java` around lines 25 - 31, FakeOrderRepository currently exposes its internal mutable lists via getOrders() and getItems(), breaking test isolation; change both methods (getOrders and getItems in FakeOrderRepository) to return defensive copies or unmodifiable views (e.g., new ArrayList<>(orders) or Collections.unmodifiableList(...)) and ensure null-safety if fields may be null, and add unit tests that attempt to modify the returned lists and assert the repository's internal state is unchanged.apps/commerce-api/src/test/java/com/loopers/domain/like/LikeUnitTest.java (1)
33-53: 예외 테스트에 경계값(0/음수) 케이스가 빠져 있다.운영 관점에서 경계값 누락은 잘못된 입력이 상위 계층까지 전파되는 회귀를 놓치게 만든다.
userId/productId에 대해0,-1입력 시BAD_REQUEST가 발생하는 테스트를 추가해 도메인 규칙을 고정하는 것이 좋다. JUnit@ParameterizedTest로 케이스를 묶으면 유지보수가 수월하다.As per coding guidelines:
**/*Test*.java단위 테스트는 경계값/실패 케이스/예외 흐름을 포함해야 한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeUnitTest.java` around lines 33 - 53, Add parameterized boundary tests in LikeUnitTest that assert the Like constructor throws CoreException with ErrorType.BAD_REQUEST for invalid IDs; create two `@ParameterizedTest` methods (e.g., throwsException_whenUserIdIsInvalid and throwsException_whenProductIdIsInvalid) that pass values 0 and -1 for userId and productId respectively and use assertThrows(() -> new Like(...)) to verify the exception and that result.getErrorType() equals ErrorType.BAD_REQUEST; keep existing null tests and group these new cases with JUnit parameterized inputs to cover boundary/negative values for the Like constructor.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java (1)
50-52:totalPrice()연산에서 오버플로우 가능성이 이론적으로 존재한다.
productPrice * quantity에서 두 값이 모두 매우 큰 경우long범위를 초과할 수 있다. 현실적으로 발생 확률은 낮지만, 금액 계산이므로Math.multiplyExact()를 사용하면 오버플로우 시ArithmeticException을 발생시켜 조기에 문제를 감지할 수 있다.🛡️ 오버플로우 방어 코드
public long totalPrice() { - return productPrice * quantity; + return Math.multiplyExact(productPrice, quantity.longValue()); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java` around lines 50 - 52, The totalPrice() method currently returns productPrice * quantity which can overflow silently; replace the raw multiplication in OrderItem.totalPrice() with Math.multiplyExact(productPrice, quantity) to have an ArithmeticException thrown on overflow, or catch that exception and rethrow/wrap with a clearer message including productPrice and quantity for diagnostics.apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java (1)
12-12:findByUserId가 페이징 없이 전체 목록을 반환한다.사용자가 대량의 좋아요를 등록한 경우 메모리 부족 또는 응답 지연이 발생할 수 있다. 운영 환경에서 OOM이나 GC 부하로 이어질 수 있으므로, 페이징 처리(
Pageable파라미터 도입) 또는 반환 개수 제한을 고려해야 한다.♻️ 페이징 적용 예시
+import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; - List<Like> findByUserId(Long userId); + Page<Like> findByUserId(Long userId, Pageable pageable);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java` at line 12, The current LikeRepository.findByUserId(Long userId) returns an unbounded List and can OOM; change the repository to support pagination by replacing it with a paged signature like Page<Like> findByUserId(Long userId, Pageable pageable) (or Slice<Like> if you prefer) in the LikeRepository interface, then update all callers (services/controllers/tests) to accept/pass a Pageable and handle the Page response (total/pages/content) appropriately; ensure imports for org.springframework.data.domain.Pageable and Page are added and adjust any mapping/serialization logic that assumed a List.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java (1)
3-8:OrderItem관련 메서드가OrderRepository에 포함되어 있다.DDD aggregate root 관점에서
Order가OrderItem의 lifecycle을 관리한다면 이 설계가 적절하다. 그러나 현재saveItem만 존재하고findItemsByOrderId등의 조회 메서드가 없어, 향후 주문 조회 시 별도 처리가 필요하다.Order.java의@Transient이슈와 연결되므로 함께 고려해야 한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java` around lines 3 - 8, OrderRepository currently exposes saveItem(OrderItem) which breaks aggregate boundaries: either move OrderItem persistence into the Order aggregate API or introduce a dedicated repository and necessary query methods. Remove saveItem from OrderRepository and ensure OrderRepository.save(Order) persists its items via the aggregate (adjust Order.java so items are persisted rather than `@Transient`), or create a new OrderItemRepository with save(OrderItem) and findItemsByOrderId(orderId) to support item lookups; update callers to use OrderRepository.save(Order) or OrderItemRepository.findItemsByOrderId and remove direct saveItem usages.apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java (1)
11-13:@Component대신@Repository사용을 권장한다.
@Repository는 데이터 접근 계층임을 명시적으로 표현하고, Spring의 PersistenceExceptionTranslationPostProcessor가 JPA 예외를 DataAccessException으로 변환하는 혜택을 받을 수 있다. 운영 시 예외 추적 및 분류가 용이해진다.♻️ 어노테이션 변경
-import org.springframework.stereotype.Component; +import org.springframework.stereotype.Repository; `@RequiredArgsConstructor` -@Component +@Repository public class LikeRepositoryImpl implements LikeRepository {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java` around lines 11 - 13, Replace the `@Component` annotation on the LikeRepositoryImpl class with `@Repository` so the implementation (class LikeRepositoryImpl implements LikeRepository) is explicitly marked as a persistence component and benefits from Spring's PersistenceExceptionTranslationPostProcessor (converting JPA exceptions into DataAccessException); update imports accordingly to org.springframework.stereotype.Repository and remove or replace the existing org.springframework.stereotype.Component import.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
13-21: OrderItem 개별 저장으로 인한 성능 저하 가능성이 있다.
for루프 내에서orderRepository.saveItem(item)을 반복 호출하면 주문 항목 수만큼 INSERT 쿼리가 발생한다. 대량 주문 시 DB 라운드트립이 증가하여 응답 지연이 발생할 수 있다.현재는 주문당 항목 수가 적다면 문제없으나, 향후 대량 주문 지원 시
saveAll()배치 저장으로 전환을 고려해야 한다.♻️ 수정안: 배치 저장 방식
`@Transactional` public Order createOrder(Order order) { Order savedOrder = orderRepository.save(order); - for (OrderItem item : order.getOrderItems()) { - item.assignOrderId(savedOrder.getId()); - orderRepository.saveItem(item); - } + order.getOrderItems().forEach(item -> item.assignOrderId(savedOrder.getId())); + orderRepository.saveAllItems(order.getOrderItems()); return savedOrder; }
OrderRepository에saveAllItems(List<OrderItem>)메서드 추가 필요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java` around lines 13 - 21, The createOrder method in OrderService calls orderRepository.saveItem in a loop causing one INSERT per OrderItem; change this to batch insert to avoid N DB roundtrips: add a new repository method OrderRepository.saveAllItems(List<OrderItem>) and in OrderService.createOrder collect order.getOrderItems(), call item.assignOrderId(savedOrder.getId()) for each item, then call orderRepository.saveAllItems(items) instead of per-item saveItem; ensure saveAllItems performs a single batch insert (or uses JPA saveAll) to improve performance.apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
48-49: 포인트 차감 순서가 주문 생성보다 앞서 수행된다.
user.deductPoint()가orderService.createOrder()이전에 호출된다. 주문 저장 실패 시 트랜잭션 롤백으로 포인트도 복원되나, 비즈니스 흐름상 주문 성공 후 결제(포인트 차감)가 더 자연스럽다. 현재 순서는 기능상 문제는 없으나 코드 가독성 측면에서 검토가 필요하다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java` around lines 48 - 49, The point deduction currently happens before the Order is persisted (user.deductPoint(...) is called immediately after new Order(...)); move the point deduction to occur after the order is created/saved (e.g., after calling orderService.createOrder(...) or the persistence method that stores the Order) so the business flow reads: create/save Order then deduct points; keep the same transaction/context to ensure rollback behavior and update any comments/variable names around Order order = new Order(userId, orderItems) and user.deductPoint(...) to reflect the new order-then-payment sequence.apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java (1)
50-56: increaseStock에서 정수 오버플로우 가능성이 있다.
current + quantity가Integer.MAX_VALUE를 초과할 경우 음수로 래핑되어 잘못된 재고 값이 저장될 수 있다. 현실적으로 발생 확률은 낮으나, 방어적 코딩 관점에서 검증 추가를 권장한다.♻️ 수정안: 오버플로우 검증 추가
public void increaseStock(int quantity) { if (quantity < 1) { throw new CoreException(ErrorType.BAD_REQUEST, "복원 수량은 1 이상이어야 합니다"); } int current = this.stockQuantity.value(); + if (current > Integer.MAX_VALUE - quantity) { + throw new CoreException(ErrorType.BAD_REQUEST, "재고 수량이 최대값을 초과합니다"); + } this.stockQuantity = new StockQuantity(current + quantity); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java` around lines 50 - 56, increaseStock can overflow when computing current + quantity; before constructing new StockQuantity perform a safe check (e.g., if (Integer.MAX_VALUE - current < quantity)) and throw a CoreException(ErrorType.BAD_REQUEST, "복원 수량이 허용 범위를 초과합니다") or similar if overflow would occur. Update the increaseStock method to validate that current + quantity does not exceed Integer.MAX_VALUE and only then assign this.stockQuantity = new StockQuantity(current + quantity).apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java (2)
76-95: 좋아요가 존재하지 않을 때의 unlike 동작 테스트가 누락되었다.운영 환경에서 사용자가 좋아요하지 않은 상품에 대해 unlike를 호출할 수 있다. 이 경우 예외를 발생시킬지, 무시할지에 대한 테스트가 필요하다. 예외 없이 처리한다면 멱등성이 보장되어 프론트엔드에서 상태 동기화가 용이하지만, 이 동작을 명시적으로 테스트해야 한다.
🧪 누락된 테스트 케이스 제안
`@DisplayName`("좋아요가 존재하지 않으면, 예외 없이 종료된다.") `@Test` void doesNothing_whenLikeNotExists() { // arrange Long userId = 1L; Long productId = 100L; // 좋아요를 등록하지 않은 상태 // act & assert assertDoesNotThrow(() -> likeService.unlike(userId, productId)); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java` around lines 76 - 95, Add a test in LikeServiceIntegrationTest (inside the Unlike nested class) covering the case where a like does not exist: call likeService.unlike(userId, productId) without first creating a like and assert it does not throw (use assertDoesNotThrow) to ensure idempotent behavior; reference the existing deletesLike_whenExists test for structure and use the same userId/productId setup and likeJpaRepository for verification where needed.
40-57: 경계값 및 예외 케이스 테스트가 부족하다.통합 테스트에서 정상 플로우만 검증하고 있다. 운영 장애 예방을 위해 다음 케이스 추가를 권장한다:
userId또는productId가null인 경우- 동시에 여러 스레드에서 같은 사용자/상품 조합으로 like 호출 시 중복 생성 방지 검증
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java` around lines 40 - 57, LikeServiceIntegrationTest currently only covers the happy path; add tests in LikeServiceIntegrationTest that verify invalid inputs and concurrency: 1) add parameterized or separate tests that call likeService.like(null, productId) and likeService.like(userId, null) and assert the expected exception (e.g., IllegalArgumentException/NullPointerException) is thrown and no entity is persisted via likeJpaRepository.findByUserIdAndProductId; 2) add a concurrent test that spawns multiple threads or uses an ExecutorService to call likeService.like(sameUserId, sameProductId) concurrently, then assert only one Like record exists (query likeJpaRepository.findByUserIdAndProductId and/or count by userId/productId) to ensure duplicate creation is prevented.apps/commerce-api/src/main/java/com/loopers/domain/user/User.java (1)
33-41: deductPoint 메서드에서 포인트 잔액 검증 후 차감 로직이 적절하다.비즈니스 규칙(최소 차감량 1, 잔액 부족 시 예외)이 도메인 엔티티에 캡슐화되어 있다. 다만, 동시성 환경에서 여러 트랜잭션이 동시에 포인트를 차감하면 레이스 컨디션이 발생할 수 있다. 운영 환경에서는 비관적 락(
@Lock(PESSIMISTIC_WRITE)) 또는 낙관적 락(@Version) 적용을 고려해야 한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java` around lines 33 - 41, The deductPoint method in User currently validates and subtracts points but is vulnerable to race conditions under concurrent transactions; update the User entity to support concurrency control by adding optimistic locking with a version field (e.g., a `@Version` long version) or prepare for pessimistic locking usage at repository/service level (e.g., methods annotated with `@Lock`(PESSIMISTIC_WRITE)); ensure User.deductPoint remains unchanged functionally but is used alongside the chosen locking strategy so concurrent deductions are serialized safely.apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeUnitTest.java (1)
74-118: NOT_FOUND 예외 경로 테스트가 누락되어 있다.운영 관점에서 사용자/상품 조회 실패 경로를 검증하지 않으면 예외 매핑 회귀 시 500 응답으로 변질되어도 테스트에서 조기 탐지가 어렵다.
수정안으로userId미존재,productId미존재 각각에 대해CoreException의ErrorType.NOT_FOUND를 검증하는 테스트를 추가하는 것이 좋다.
추가 테스트로throwsException_whenUserNotFound,throwsException_whenProductNotFound2개 케이스를 포함하는 것이 좋다.As per coding guidelines "단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다."를 근거로 한 제안이다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeUnitTest.java` around lines 74 - 118, Add two unit tests to cover NOT_FOUND paths: implement throwsException_whenUserNotFound and throwsException_whenProductNotFound that call orderFacade.createOrder and assert a CoreException with ErrorType.NOT_FOUND; for the user-missing case, do not add a user to fakeUserRepository (but ensure a valid product exists in fakeProductRepository) and verify the exception message/contents indicate missing user, and for the product-missing case, add a valid user to fakeUserRepository but omit the product from fakeProductRepository and verify the CoreException ErrorType.NOT_FOUND and message indicate missing product; use the same test patterns/assertions as the existing stock/point tests to keep style consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`:
- Around line 27-51: The createOrder method in OrderFacade currently reads
Product via productRepository.findById(...) then calls
product.decreaseStock(...), which can cause race conditions; fix by loading the
Product with a DB lock or optimistic version and adding retry: either add a
repository method in ProductJpaRepository e.g. findByIdForUpdate(Long id)
annotated with `@Lock`(LockModeType.PESSIMISTIC_WRITE) (or a FOR UPDATE query) and
use that in OrderFacade.createOrder when fetching products, or add a `@Version`
field to Product and implement retry logic around product.decreaseStock() to
handle OptimisticLockException (retry N times and then fail); also add an
integration test that simulates concurrent orders for the same product to assert
no overselling occurs.
In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java`:
- Around line 12-13: The Brand entity's name field is a non-scalar record
(BrandName) so JPA cannot map it as-is; add `@Convert`(converter =
BrandNameConverter.class) to the Brand.name field and implement an
AttributeConverter<BrandName,String> named BrandNameConverter that converts
BrandName.value() to String for database writes and reconstructs BrandName from
String on reads; alternatively mark BrandName as `@Embeddable` and map it
accordingly. After implementing the converter (or embeddable), add a DataJpaTest
that saves and loads a Brand (and exercises
BrandRepository.existsByName(BrandName)) to verify round-trip
serialization/deserialization works.
In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java`:
- Around line 16-20: Replace the current check-then-act flow in BrandService
(the existsByName + new Brand(name) + brandRepository.save(...) sequence) by
attempting the save directly and converting any persistent-uniqueness violation
into a CoreException with ErrorType.CONFLICT; specifically, remove reliance on
brandRepository.existsByName, call brandRepository.save(brand) inside a
try/catch that catches the DB/ORM uniqueness exception (e.g.,
DataIntegrityViolationException or the specific persistence exception your stack
throws) and rethrow new CoreException(ErrorType.CONFLICT, "이미 등록된 브랜드 이름입니다");
also add a concurrent integration test that issues two simultaneous create
requests for the same name and asserts one succeeds and the other receives
CoreException -> CONFLICT handled by ApiControllerAdvice for consistent response
formatting.
- Around line 6-10: Remove the Spring framework annotation from the domain
service so the domain stays framework-free: delete the `@Component` on the
BrandService class (and keep it a plain POJO, leaving RequiredArgsConstructor if
needed), then register/compose BrandService as a Spring bean in the
Application/Infrastructure layer (create a `@Configuration` or factory in the
application module that constructs BrandService with its dependencies). After
moving bean registration, add an application-layer integration/use-case test to
verify the service is correctly composed without introducing framework
annotations into the domain package.
In `@apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java`:
- Around line 22-31: The Like constructor currently only rejects nulls but
allows 0/negative IDs; update the Like(Long userId, Long productId) constructor
to validate that userId and productId are > 0 and throw new
CoreException(ErrorType.BAD_REQUEST, "...") with clear messages when they are <=
0; add unit tests for the Like constructor to assert CoreException is thrown for
userId 0, userId -1, productId 0, and productId -1 (separately) to ensure
domain-level rejection of non-positive IDs and consistent error routing via
CoreException/ApiControllerAdvice.
- Around line 10-18: Add a DB-level unique constraint and index on the Like
entity to prevent duplicate userId+productId rows: modify the `@Table` on class
Like to declare a UniqueConstraint across columns "user_id" and "product_id"
(and optionally a supporting index) so the database enforces uniqueness; keep
the existing fields userId and productId in class Like (extends BaseEntity).
Also add an integration/ concurrency test that issues concurrent create-like
requests for the same userId/productId and asserts only one Like row is
persisted.
In `@apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java`:
- Around line 10-16: Add proper DB indexes for the Like entity and ensure
migrations are applied: modify the Like entity to include a
`@Table`(...indexes=...) declaration (e.g., add indexes named idx_user_id on
user_id and idx_product_id on product_id, or a composite idx_user_product on
user_id,product_id) so queries implemented by repository methods
findByUserIdAndProductId, findByUserId, and countByProductId use indexes;
because hibernate.ddl-auto is disabled in prod, also create and commit a
migration (Flyway or Liquibase) that creates these indexes and deploy it to
dev/qa/prd, then verify the indexes exist in each environment and run EXPLAIN
for the above methods to confirm the query plans use the new indexes.
In `@apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java`:
- Around line 15-20: The like(Long userId, Long productId) path is vulnerable to
TOCTOU races; add a DB unique constraint on (user_id, product_id) in the Like
entity/migration and change LikeService.like to attempt save and, on
unique-constraint/DataIntegrityViolationException, requery via
likeRepository.findByUserIdAndProductId(userId, productId) and return the
existing record to make the operation idempotent; keep the optimistic
read-or-create fallback if desired but ensure the save is wrapped in a
transaction and exception handler that returns the re-fetched Like. Also add a
concurrency integration test that fires many parallel calls to LikeService.like
(same userId/productId) and asserts that only one Like row exists and all calls
get the same logical result.
In `@apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java`:
- Around line 25-26: The Order.entity currently marks the field orderItems as
`@Transient` so it will not be restored from the DB and Order.getOrderItems() will
return an empty list on reads; to fix, either remove `@Transient` and add a proper
JPA relationship (e.g., annotate List<OrderItem> orderItems with
`@OneToMany`(mappedBy="order", cascade=..., fetch=...) and ensure OrderItem has
the corresponding `@ManyToOne` order field), or keep `@Transient` but implement a
repository method that loads OrderItem by orderId and injects them into Order
after fetch (e.g., in OrderService or a mapper), and if you intentionally leave
it transient for now add a clear TODO comment on the Order.orderItems field
referencing getOrderItems and the intended migration plan so future read APIs
won’t be broken.
In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Price.java`:
- Around line 3-4: The Price record currently allows null and negative/zero
values; add a compact constructor in Price to validate that value is non-null
and greater than 0 and throw the domain error type used across the project
(e.g., CoreException with a clear message) when validation fails. Update
references that construct Price to rely on this constructor and add unit tests
for the Price record covering null, -1, 0, and a valid positive value to assert
the correct exception is thrown for invalid inputs and successful creation for
the valid case. Ensure the thrown message is descriptive so ApiControllerAdvice
maps it properly.
In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java`:
- Around line 26-27: The Product entity's stockQuantity field (private
StockQuantity stockQuantity) lacks a JPA mapping for the custom value object
StockQuantity; fix by either making StockQuantity an `@Embeddable` and annotate
the Product.stockQuantity field with `@Embedded`, or implement an
AttributeConverter<StockQuantity,Integer> (e.g., StockQuantityConverter) and
annotate the field with `@Convert`(converter = StockQuantityConverter.class);
after applying one approach, add an integration test that persists a Product
instance to the DB to validate the JPA mapping and catch
PersistenceMappingException scenarios.
In `@apps/commerce-api/src/main/java/com/loopers/domain/product/ProductName.java`:
- Around line 3-4: ProductName currently allows null/blank values; add a compact
constructor to the ProductName record that trims the incoming value, rejects
null or blank strings and throws the domain validation exception used by the
project (e.g., CoreException or the standard domain validation helper) with a
clear message; update usages that construct ProductName to rely on this
invariant and add unit tests covering null, empty string, blank-only string, and
a valid trimmed string to ensure behavior.
In
`@apps/commerce-api/src/main/java/com/loopers/domain/product/StockQuantity.java`:
- Around line 3-4: StockQuantity currently allows null and negative values; add
domain-layer validation in the StockQuantity record's canonical constructor
(public StockQuantity(Integer value)) to throw a CoreException with an
appropriate error message when value is null or value < 0, and keep accepted
behavior for zero and positive values; then add unit tests (e.g.,
StockQuantityTest) covering null (expect CoreException), -1 (expect
CoreException), 0 (valid), and a positive value (valid) to verify the invariant.
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java`:
- Around line 10-12: Add equals and hashCode to the User entity to base identity
on BaseEntity's id: implement User.equals(Object) to return true for same
instance, false for non-User, and compare non-null getId() values for equality;
implement User.hashCode() to return getId().hashCode() when id is non-null or 0
when null. Place these overrides in the User class alongside other methods to
ensure correct behavior in Sets/Maps and JPA persistence context.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java`:
- Around line 22-35: The controller methods like() and unlike() (and likewise
OrderV1Controller.createOrder()) currently trust a client-provided
X-Loopers-UserId header—replace that with a server-trusted authenticated
principal: remove the `@RequestHeader`("X-Loopers-UserId") Long userId parameter
and instead inject the current user from the security context (e.g.
`@AuthenticationPrincipal` or a RequestContextHolder/ThreadLocal-backed
authenticated user provider) and use that ID for operations via likeFacade;
additionally enforce and return 401/403 when no authenticated principal is
present and add controller/integration tests that assert header-based spoofing
is rejected and that operations require the authenticated user's ID (and that
any header/user mismatch is denied).
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java`:
- Around line 14-25: Remove all Bean Validation annotations from the DTOs in
OrderV1Dto: delete `@NotEmpty` on CreateRequest.items, remove `@Valid` on the List
element type, and remove `@NotNull` and `@Min`(1) from OrderItemRequest fields
(productId and quantity). Keep the record definitions and the conversion helper
OrderItemRequest.toFacadeRequest() unchanged so domain-side validation (e.g.,
OrderItem constructor checks for quantity < 1) handles errors; also remove any
now-unused validation imports in this file.
In
`@apps/commerce-api/src/test/java/com/loopers/application/order/FakeUserRepository.java`:
- Around line 23-26: FakeUserRepository의 save(User user)는 현재 store에 저장하지 않고 입력값을
그대로 반환하므로 테스트 신뢰성을 해칩니다; 수정하려면 FakeUserRepository.save 메서드가 전달받은 User를 내부
store(예: Map 또는 List)에 저장하거나 업데이트하도록 구현하고 기존 findById가 해당 store에서 조회할 수 있게 하세요;
만약 User.id가 null이면 간단한 ID 생성 로직을 추가하거나 IllegalArgumentException을 던지도록 결정하고,
대안으로는 addUserWithId를 통한 시드 전용 정책을 문서화하여 save를 업데이트 전용으로만 사용하도록 명확히 표기하세요.
---
Minor comments:
In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java`:
- Around line 14-15: OrderItem lacks entity-level validation for orderId which
can lead to null being persisted; override BaseEntity.guard() in OrderItem to
validate orderId (throw a CoreException/ErrorType.INTERNAL_ERROR with a clear
message if null) or alternatively change OrderItem to require orderId in its
constructor to ensure it's always set; locate the OrderItem class and implement
the guard() override (or constructor change) and update usages like
OrderService.createOrder(), assignOrderId(), and saveItem() to match the chosen
approach.
In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java`:
- Around line 31-37: Add validation to the Product constructor (the Product(Long
brandId, String name, Long price, String description, Integer stockQuantity)
method) to guard required fields: require non-null brandId, non-blank name, and
non-null non-negative price; throw an IllegalArgumentException (or use
Objects.requireNonNull for null checks) with clear messages when violated, and
keep existing StockQuantity creation for stockQuantity. Also add unit tests
covering null brandId, blank name, and negative price to assert the constructor
rejects invalid input.
In
`@apps/commerce-api/src/test/java/com/loopers/application/order/FakeProductRepository.java`:
- Around line 15-19: addProduct에서 생성한 Product의 내부 ID가 BaseEntity에 설정되지 않아 store의
키(sequence)와 불일치할 수 있으므로 addProduct에서 Product에 현재 sequence 값을 명시적으로 설정한 뒤
store.put(sequence++, product) 하거나(순증가 시점 주의), 테스트에서 일관성을 위해 addProductWithId를
사용하도록 코드/테스트를 변경하세요; 대상 식별자는 addProduct, sequence, store, BaseEntity,
addProductWithId입니다.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandNameUnitTest.java`:
- Around line 18-53: Add two unit tests to BrandNameUnitTest:
createsBrandName_whenValueIsExactly50Chars and
createsBrandName_whenValueHasSurroundingSpaces. For
createsBrandName_whenValueIsExactly50Chars, construct BrandName with a 50-char
string (e.g., "a".repeat(50)) and assert no exception is thrown and
brandName.value() equals that 50-char string. For
createsBrandName_whenValueHasSurroundingSpaces, construct BrandName with a value
padded with leading/trailing spaces (e.g., " 나이키 ") and assert creation succeeds
and brandName.value() equals the trimmed string ("나이키"). Use the BrandName
constructor and value() accessor and assert behavior matches existing tests'
pattern (assertThrows/assertThat).
In
`@apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceUnitTest.java`:
- Around line 42-54: Add a new unit test in BrandServiceUnitTest (alongside
throwsException_whenNameAlreadyExists) that verifies normalization/trim-based
deduplication: first call brandService.register(" 나이키 ") then assert that
brandService.register("나이키") throws CoreException with ErrorType.CONFLICT;
ensure the test uses the same assertThrows pattern and checks
result.getErrorType() to guard against regressions if trimming behavior changes.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceUnitTest.java`:
- Around line 41-56: The test only asserts fields on the returned Like but not
repository state, so update the test in LikeServiceUnitTest to assert
idempotency by checking the persistent count after the second like: call the
repository method (e.g., likeRepository.countByUserIdAndProductId(userId,
productId) or likeRepository.countByProductId(productId)) and assert it equals
1; additionally add a new test that calls likeService.like(userId, productId) N
times (e.g., loop 5 times) and then asserts the same repository count is 1 to
prove repeated calls remain idempotent. Ensure you reference the existing
likeService.like(...) flow and use the repository counting methods for the
assertions.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/product/ProductUnitTest.java`:
- Around line 75-91: Add a failing-case unit test to ProductUnitTest under the
IncreaseStock nested class to cover validation when product.increaseStock is
called with quantity < 1: create a Product instance and assert that calling
product.increaseStock(0) and product.increaseStock(-1) throws CoreException with
ErrorType.BAD_REQUEST (use assertThrows for both cases and assert the
exception's getErrorType equals ErrorType.BAD_REQUEST) so the validation logic
in Product.increaseStock is covered.
---
Nitpick comments:
In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`:
- Around line 48-49: The point deduction currently happens before the Order is
persisted (user.deductPoint(...) is called immediately after new Order(...));
move the point deduction to occur after the order is created/saved (e.g., after
calling orderService.createOrder(...) or the persistence method that stores the
Order) so the business flow reads: create/save Order then deduct points; keep
the same transaction/context to ensure rollback behavior and update any
comments/variable names around Order order = new Order(userId, orderItems) and
user.deductPoint(...) to reflect the new order-then-payment sequence.
In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java`:
- Around line 15-20: BrandService currently constructs a BrandName for
validation then reconstructs a Brand from the raw name, causing duplicated
normalization logic; change the code to construct the Brand using the
already-created BrandName (add a Brand(BrandName) constructor in Brand) and call
brandRepository.save with that Brand instance so validation/normalization is
single-path (update BrandService uses of new Brand(name) to new Brand(brandName)
and adjust/extend tests to assert normalization when saving inputs with
surrounding whitespace).
In `@apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java`:
- Line 12: The current LikeRepository.findByUserId(Long userId) returns an
unbounded List and can OOM; change the repository to support pagination by
replacing it with a paged signature like Page<Like> findByUserId(Long userId,
Pageable pageable) (or Slice<Like> if you prefer) in the LikeRepository
interface, then update all callers (services/controllers/tests) to accept/pass a
Pageable and handle the Page response (total/pages/content) appropriately;
ensure imports for org.springframework.data.domain.Pageable and Page are added
and adjust any mapping/serialization logic that assumed a List.
In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java`:
- Around line 50-52: The totalPrice() method currently returns productPrice *
quantity which can overflow silently; replace the raw multiplication in
OrderItem.totalPrice() with Math.multiplyExact(productPrice, quantity) to have
an ArithmeticException thrown on overflow, or catch that exception and
rethrow/wrap with a clearer message including productPrice and quantity for
diagnostics.
In
`@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java`:
- Around line 3-8: OrderRepository currently exposes saveItem(OrderItem) which
breaks aggregate boundaries: either move OrderItem persistence into the Order
aggregate API or introduce a dedicated repository and necessary query methods.
Remove saveItem from OrderRepository and ensure OrderRepository.save(Order)
persists its items via the aggregate (adjust Order.java so items are persisted
rather than `@Transient`), or create a new OrderItemRepository with
save(OrderItem) and findItemsByOrderId(orderId) to support item lookups; update
callers to use OrderRepository.save(Order) or
OrderItemRepository.findItemsByOrderId and remove direct saveItem usages.
In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java`:
- Around line 13-21: The createOrder method in OrderService calls
orderRepository.saveItem in a loop causing one INSERT per OrderItem; change this
to batch insert to avoid N DB roundtrips: add a new repository method
OrderRepository.saveAllItems(List<OrderItem>) and in OrderService.createOrder
collect order.getOrderItems(), call item.assignOrderId(savedOrder.getId()) for
each item, then call orderRepository.saveAllItems(items) instead of per-item
saveItem; ensure saveAllItems performs a single batch insert (or uses JPA
saveAll) to improve performance.
In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java`:
- Around line 50-56: increaseStock can overflow when computing current +
quantity; before constructing new StockQuantity perform a safe check (e.g., if
(Integer.MAX_VALUE - current < quantity)) and throw a
CoreException(ErrorType.BAD_REQUEST, "복원 수량이 허용 범위를 초과합니다") or similar if
overflow would occur. Update the increaseStock method to validate that current +
quantity does not exceed Integer.MAX_VALUE and only then assign
this.stockQuantity = new StockQuantity(current + quantity).
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java`:
- Around line 33-41: The deductPoint method in User currently validates and
subtracts points but is vulnerable to race conditions under concurrent
transactions; update the User entity to support concurrency control by adding
optimistic locking with a version field (e.g., a `@Version` long version) or
prepare for pessimistic locking usage at repository/service level (e.g., methods
annotated with `@Lock`(PESSIMISTIC_WRITE)); ensure User.deductPoint remains
unchanged functionally but is used alongside the chosen locking strategy so
concurrent deductions are serialized safely.
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java`:
- Around line 11-13: Replace the `@Component` annotation on the LikeRepositoryImpl
class with `@Repository` so the implementation (class LikeRepositoryImpl
implements LikeRepository) is explicitly marked as a persistence component and
benefits from Spring's PersistenceExceptionTranslationPostProcessor (converting
JPA exceptions into DataAccessException); update imports accordingly to
org.springframework.stereotype.Repository and remove or replace the existing
org.springframework.stereotype.Component import.
In
`@apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeUnitTest.java`:
- Around line 74-118: Add two unit tests to cover NOT_FOUND paths: implement
throwsException_whenUserNotFound and throwsException_whenProductNotFound that
call orderFacade.createOrder and assert a CoreException with
ErrorType.NOT_FOUND; for the user-missing case, do not add a user to
fakeUserRepository (but ensure a valid product exists in fakeProductRepository)
and verify the exception message/contents indicate missing user, and for the
product-missing case, add a valid user to fakeUserRepository but omit the
product from fakeProductRepository and verify the CoreException
ErrorType.NOT_FOUND and message indicate missing product; use the same test
patterns/assertions as the existing stock/point tests to keep style consistent.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java`:
- Around line 76-95: Add a test in LikeServiceIntegrationTest (inside the Unlike
nested class) covering the case where a like does not exist: call
likeService.unlike(userId, productId) without first creating a like and assert
it does not throw (use assertDoesNotThrow) to ensure idempotent behavior;
reference the existing deletesLike_whenExists test for structure and use the
same userId/productId setup and likeJpaRepository for verification where needed.
- Around line 40-57: LikeServiceIntegrationTest currently only covers the happy
path; add tests in LikeServiceIntegrationTest that verify invalid inputs and
concurrency: 1) add parameterized or separate tests that call
likeService.like(null, productId) and likeService.like(userId, null) and assert
the expected exception (e.g., IllegalArgumentException/NullPointerException) is
thrown and no entity is persisted via
likeJpaRepository.findByUserIdAndProductId; 2) add a concurrent test that spawns
multiple threads or uses an ExecutorService to call likeService.like(sameUserId,
sameProductId) concurrently, then assert only one Like record exists (query
likeJpaRepository.findByUserIdAndProductId and/or count by userId/productId) to
ensure duplicate creation is prevented.
In `@apps/commerce-api/src/test/java/com/loopers/domain/like/LikeUnitTest.java`:
- Around line 33-53: Add parameterized boundary tests in LikeUnitTest that
assert the Like constructor throws CoreException with ErrorType.BAD_REQUEST for
invalid IDs; create two `@ParameterizedTest` methods (e.g.,
throwsException_whenUserIdIsInvalid and throwsException_whenProductIdIsInvalid)
that pass values 0 and -1 for userId and productId respectively and use
assertThrows(() -> new Like(...)) to verify the exception and that
result.getErrorType() equals ErrorType.BAD_REQUEST; keep existing null tests and
group these new cases with JUnit parameterized inputs to cover boundary/negative
values for the Like constructor.
In
`@apps/commerce-api/src/test/java/com/loopers/domain/order/FakeOrderRepository.java`:
- Around line 25-31: FakeOrderRepository currently exposes its internal mutable
lists via getOrders() and getItems(), breaking test isolation; change both
methods (getOrders and getItems in FakeOrderRepository) to return defensive
copies or unmodifiable views (e.g., new ArrayList<>(orders) or
Collections.unmodifiableList(...)) and ensure null-safety if fields may be null,
and add unit tests that attempt to modify the returned lists and assert the
repository's internal state is unchanged.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (7)
CLAUDE.mdis excluded by!**/*.mdand included by**docs/architecture.mdis excluded by!**/*.mdand included by**docs/design/01-requirements.mdis excluded by!**/*.mdand included by**docs/design/02-sequence-diagrams.mdis excluded by!**/*.mdand included by**docs/design/03-class-diagram.mdis excluded by!**/*.mdand included by**docs/design/04-erd.mdis excluded by!**/*.mdand included by**skill.mdis excluded by!**/*.mdand included by**
📒 Files selected for processing (48)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.javaapps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.javaapps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.javaapps/commerce-api/src/main/java/com/loopers/domain/brand/BrandName.javaapps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.javaapps/commerce-api/src/main/java/com/loopers/domain/like/Like.javaapps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.javaapps/commerce-api/src/main/java/com/loopers/domain/order/Order.javaapps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.javaapps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.javaapps/commerce-api/src/main/java/com/loopers/domain/product/Price.javaapps/commerce-api/src/main/java/com/loopers/domain/product/Product.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductName.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/product/StockQuantity.javaapps/commerce-api/src/main/java/com/loopers/domain/user/User.javaapps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.javaapps/commerce-api/src/test/java/com/loopers/application/order/FakeProductRepository.javaapps/commerce-api/src/test/java/com/loopers/application/order/FakeUserRepository.javaapps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeUnitTest.javaapps/commerce-api/src/test/java/com/loopers/domain/brand/BrandNameUnitTest.javaapps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceUnitTest.javaapps/commerce-api/src/test/java/com/loopers/domain/brand/BrandUnitTest.javaapps/commerce-api/src/test/java/com/loopers/domain/brand/FakeBrandRepository.javaapps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceUnitTest.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeUnitTest.javaapps/commerce-api/src/test/java/com/loopers/domain/order/FakeOrderRepository.javaapps/commerce-api/src/test/java/com/loopers/domain/product/ProductUnitTest.java
| @Transactional | ||
| public OrderInfo createOrder(Long userId, List<OrderItemRequest> itemRequests) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다")); | ||
|
|
||
| List<OrderItem> orderItems = new ArrayList<>(); | ||
| for (OrderItemRequest req : itemRequests) { | ||
| Product product = productRepository.findById(req.productId()) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다")); | ||
|
|
||
| product.decreaseStock(req.quantity()); | ||
|
|
||
| OrderItem orderItem = new OrderItem( | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getPrice(), | ||
| req.quantity() | ||
| ); | ||
| orderItems.add(orderItem); | ||
| } | ||
|
|
||
| Order order = new Order(userId, orderItems); | ||
| user.deductPoint(order.getTotalPrice()); | ||
|
|
||
| return OrderInfo.from(orderService.createOrder(order)); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find Product entity and related classes
find . -name "Product.java" -o -name "ProductRepository.java" -o -name "OrderService.java" | grep -v targetRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 319
🏁 Script executed:
# Look for Product entity with decreaseStock method
rg -n "class Product" --type java -A 20 | head -100Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 6291
🏁 Script executed:
# Check for `@Version`, `@Lock` annotations in the codebase
rg -n "@Version|@Lock" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Search for decreaseStock method implementation
rg -n "decreaseStock" --type java -B 2 -A 5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5978
🏁 Script executed:
# Read full Product.java to check for `@Version` and complete implementation
cat -n apps/commerce-api/src/main/java/com/loopers/domain/product/Product.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2552
🏁 Script executed:
# Read OrderService.java implementation
cat -n apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 859
🏁 Script executed:
# Search for concurrent order or stock tests
find . -name "*Test.java" -o -name "*IT.java" | xargs grep -l "concurrent\|Thread\|stock" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Check ProductJpaRepository for any lock annotations
rg -n "ProductJpaRepository" --type java -A 10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1718
동시 주문 시 재고 차감 경쟁 조건(Race Condition) 발생 가능성이 있다.
동일 상품에 대해 동시에 여러 주문이 발생할 경우, product.decreaseStock()의 재고 확인 로직이 트랜잭션 격리 수준에서 보호되지 않아 과다 판매(overselling)가 발생할 수 있다. 예를 들어 재고 1개인 상품에 두 주문이 동시에 들어오면, 두 트랜잭션이 각각 재고 확인 후 모두 통과하여 재고가 음수로 될 수 있다.
운영 환경에서 재고 정합성 문제는 CS 이슈, 환불 처리 비용, 고객 신뢰도 하락으로 이어진다.
수정 방안:
- 비관적 잠금:
ProductJpaRepository에findById()를 오버라이드하여@Lock(LockModeType.PESSIMISTIC_WRITE)적용 또는@Query("SELECT p FROM Product p WHERE p.id = :id FOR UPDATE")로 변경 - 낙관적 잠금: Product 엔티티에
@Version필드 추가 및 재시도 로직 구현 - 분산 락: 고성능 요구 시 Redis 기반 락 적용
추가로 동시 주문에 대한 통합 테스트(여러 스레드에서 동일 상품 주문 시뮬레이션)를 작성하여 재고 정합성 검증이 필요하다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`
around lines 27 - 51, The createOrder method in OrderFacade currently reads
Product via productRepository.findById(...) then calls
product.decreaseStock(...), which can cause race conditions; fix by loading the
Product with a DB lock or optimistic version and adding retry: either add a
repository method in ProductJpaRepository e.g. findByIdForUpdate(Long id)
annotated with `@Lock`(LockModeType.PESSIMISTIC_WRITE) (or a FOR UPDATE query) and
use that in OrderFacade.createOrder when fetching products, or add a `@Version`
field to Product and implement retry logic around product.decreaseStock() to
handle OptimisticLockException (retry N times and then fail); also add an
integration test that simulates concurrent orders for the same product to assert
no overselling occurs.
| @Column(name = "name", nullable = false, unique = true, length = 50) | ||
| private BrandName name; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# BrandName 정의와 JPA 매핑 확인
rg -n --type=java 'record\s+BrandName|class\s+BrandName|@Embeddable|AttributeConverter<\s*BrandName|@Convert'Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 307
🏁 Script executed:
#!/bin/bash
# Brand.java 전체 파일 조회
find . -path "*/domain/brand/Brand.java" -type f | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 154
🏁 Script executed:
#!/bin/bash
# Brand.java 파일 크기 및 내용 확인
wc -l apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 155
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 773
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandName.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 695
🏁 Script executed:
# 컨버터나 임베디드 매핑이 전체 domain/brand 디렉토리에 있는지 확인
rg -n --type=java '@Embeddable|AttributeConverter|@Convert' apps/commerce-api/src/main/java/com/loopers/domain/brand/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 전체 도메인 패키지에서 BrandName 관련 컨버터 검색
rg -n --type=java 'BrandNameConverter|AttributeConverter.*BrandName|BrandName.*AttributeConverter' apps/commerce-api/src/main/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 스프링 설정, JPA 설정 확인 (config, configuration 디렉토리)
find apps/commerce-api/src/main/java -name '*Config*.java' -o -name '*Configuration*.java' | xargs grep -l "BrandName\|@Converter\|AttributeConverter" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 69
🏁 Script executed:
# BrandNameUnitTest 내용 확인 (테스트가 어떤 수준인지)
cat -n apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandNameUnitTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2260
🏁 Script executed:
# BaseEntity 확인 (공통 설정이 있을 가능성)
find apps/commerce-api/src/main/java -name "BaseEntity.java" | xargs cat -nRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 혹시 application.yml, application.properties에서 JPA 설정이 있는지 확인
find apps/commerce-api/src -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1044
🏁 Script executed:
# 혹시 다른 곳에 BrandName 관련 컨버터/처리가 있는지 더 광범위 검색
rg -i "brandname" apps/commerce-api/src/main/java --type=javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 973
🏁 Script executed:
# jpa.yml 파일 내용 확인
find apps/commerce-api -name "jpa.yml" | xargs cat -nRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# BaseEntity.java 경로 및 내용 다시 확인
find apps/commerce-api -name "BaseEntity.java" -exec wc -l {} \; -exec cat -n {} \;Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
JPA 값 객체 매핑 정의가 없어 런타임 영속화 실패 위험이 크다.
BrandName은 record 타입이며 기본 스칼라 타입이 아니므로, @Convert 어노테이션과 AttributeConverter<BrandName, String> 구현이나 @Embeddable 전략이 명시되지 않으면 JPA가 필드를 매핑할 수 없다. 현재 @Column만으로는 record 직렬화/역직렬화가 정의되지 않아, 애플리케이션 기동 시 매핑 오류가 발생하거나 BrandRepository.existsByName(BrandName) 같은 쿼리 메서드 호출 시 실패할 수 있다. 운영 배포 직후 장애로 이어질 수 있다.
수정 방법: @Convert(converter = BrandNameConverter.class) 어노테이션을 추가하고 AttributeConverter<BrandName, String> 구현체를 작성하여 value() ↔ String 변환을 처리해야 한다. 또는 BrandName을 @Embeddable 전략으로 변경할 수 있다.
추가 테스트: @DataJpaTest에서 Brand 엔티티의 저장/조회 라운드트립 테스트를 추가하여 BrandName의 직렬화/역직렬화가 보장되는지 검증해야 한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java` around
lines 12 - 13, The Brand entity's name field is a non-scalar record (BrandName)
so JPA cannot map it as-is; add `@Convert`(converter = BrandNameConverter.class)
to the Brand.name field and implement an AttributeConverter<BrandName,String>
named BrandNameConverter that converts BrandName.value() to String for database
writes and reconstructs BrandName from String on reads; alternatively mark
BrandName as `@Embeddable` and map it accordingly. After implementing the
converter (or embeddable), add a DataJpaTest that saves and loads a Brand (and
exercises BrandRepository.existsByName(BrandName)) to verify round-trip
serialization/deserialization works.
| import org.springframework.stereotype.Component; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class BrandService { |
There was a problem hiding this comment.
도메인 계층에 Spring 컴포넌트 의존이 섞여 아키텍처 경계가 약화된다.
Line 6~10의 @Component는 도메인 모델을 프레임워크에 고정시켜 재사용성과 테스트 격리를 저하시킨다. 도메인 서비스는 순수 객체로 유지하고, 빈 등록과 조합은 Application/Infrastructure 계층으로 이동하는 편이 운영 변경(프레임워크 교체, 배치/메시지 진입점 추가)에 안전하다. 추가로 Application 계층에서 유스케이스 조합 테스트를 두어 경계가 유지되는지 검증해야 한다.
As per coding guidelines "**/domain/**/*.java: 도메인 규칙과 인프라 관심사가 섞이면 분리하도록 제안한다."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java`
around lines 6 - 10, Remove the Spring framework annotation from the domain
service so the domain stays framework-free: delete the `@Component` on the
BrandService class (and keep it a plain POJO, leaving RequiredArgsConstructor if
needed), then register/compose BrandService as a Spring bean in the
Application/Infrastructure layer (create a `@Configuration` or factory in the
application module that constructs BrandService with its dependencies). After
moving bean registration, add an application-layer integration/use-case test to
verify the service is correctly composed without introducing framework
annotations into the domain package.
| if (brandRepository.existsByName(brandName)) { | ||
| throw new CoreException(ErrorType.CONFLICT, "이미 등록된 브랜드 이름입니다"); | ||
| } | ||
| Brand brand = new Brand(name); | ||
| return brandRepository.save(brand); |
There was a problem hiding this comment.
사전 중복 확인 후 저장하는 흐름은 동시성 상황에서 오류 응답 일관성을 깨뜨린다.
Line 16~20은 check-then-act 구조라 동시 요청에서 둘 다 통과한 뒤 저장 시점 충돌이 발생할 수 있다. 운영에서는 간헐적 500 계열 오류와 비일관 에러 포맷으로 이어진다. DB unique 제약을 최종 보루로 두고, 저장 단계의 중복 충돌을 CoreException(CONFLICT)로 일관 변환하는 경로를 추가해야 한다. 추가로 동일 브랜드명 동시 등록 통합 테스트(동시 2요청, 1건 성공/1건 CONFLICT)를 작성해야 한다.
Based on learnings "enforce unified error handling by routing errors through CoreException to ApiControllerAdvice to ensure a consistent response format."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java`
around lines 16 - 20, Replace the current check-then-act flow in BrandService
(the existsByName + new Brand(name) + brandRepository.save(...) sequence) by
attempting the save directly and converting any persistent-uniqueness violation
into a CoreException with ErrorType.CONFLICT; specifically, remove reliance on
brandRepository.existsByName, call brandRepository.save(brand) inside a
try/catch that catches the DB/ORM uniqueness exception (e.g.,
DataIntegrityViolationException or the specific persistence exception your stack
throws) and rethrow new CoreException(ErrorType.CONFLICT, "이미 등록된 브랜드 이름입니다");
also add a concurrent integration test that issues two simultaneous create
requests for the same name and asserts one succeeds and the other receives
CoreException -> CONFLICT handled by ApiControllerAdvice for consistent response
formatting.
| @Entity | ||
| @Table(name = "likes") | ||
| public class Like extends BaseEntity { | ||
|
|
||
| @Column(name = "user_id", nullable = false) | ||
| private Long userId; | ||
|
|
||
| @Column(name = "product_id", nullable = false) | ||
| private Long productId; |
There was a problem hiding this comment.
좋아요 중복 방지 제약이 없어 동시성 상황에서 데이터가 중복될 수 있다.
운영 관점에서 동일 사용자-상품 좋아요가 중복 저장되면 카운트가 부정확해지고 정합성 장애로 이어진다. user_id + product_id 유니크 제약(및 조회용 인덱스)을 테이블 레벨에 추가해 DB에서 최종 방어선을 두는 것이 필요하다. 동시 요청(또는 중복 저장) 통합 테스트를 추가해 1건만 유지되는지 검증해야 한다.
수정 예시
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.persistence.Index;
`@Entity`
-@Table(name = "likes")
+@Table(
+ name = "likes",
+ uniqueConstraints = {
+ `@UniqueConstraint`(name = "uk_likes_user_product", columnNames = {"user_id", "product_id"})
+ },
+ indexes = {
+ `@Index`(name = "idx_likes_user_id", columnList = "user_id"),
+ `@Index`(name = "idx_likes_product_id", columnList = "product_id")
+ }
+)
public class Like extends BaseEntity {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java` around
lines 10 - 18, Add a DB-level unique constraint and index on the Like entity to
prevent duplicate userId+productId rows: modify the `@Table` on class Like to
declare a UniqueConstraint across columns "user_id" and "product_id" (and
optionally a supporting index) so the database enforces uniqueness; keep the
existing fields userId and productId in class Like (extends BaseEntity). Also
add an integration/ concurrency test that issues concurrent create-like requests
for the same userId/productId and asserts only one Like row is persisted.
| public record StockQuantity(Integer value) { | ||
| } |
There was a problem hiding this comment.
재고 수량 불변식이 없어 음수/NULL 재고가 허용된다.
운영 관점에서 음수 재고가 유입되면 주문 가능 여부 판단과 차감 로직이 왜곡되어 장애성 이슈로 연결된다. StockQuantity에서 null 및 0 미만 값을 차단해야 한다. 추가로 null, -1, 0, 양수 값에 대한 단위 테스트를 추가하는 것이 필요하다.
수정 예시
package com.loopers.domain.product;
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
+
public record StockQuantity(Integer value) {
+ public StockQuantity {
+ if (value == null) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "재고는 필수입니다");
+ }
+ if (value < 0) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다");
+ }
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public record StockQuantity(Integer value) { | |
| } | |
| package com.loopers.domain.product; | |
| import com.loopers.support.error.CoreException; | |
| import com.loopers.support.error.ErrorType; | |
| public record StockQuantity(Integer value) { | |
| public StockQuantity { | |
| if (value == null) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 필수입니다"); | |
| } | |
| if (value < 0) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다"); | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/domain/product/StockQuantity.java`
around lines 3 - 4, StockQuantity currently allows null and negative values; add
domain-layer validation in the StockQuantity record's canonical constructor
(public StockQuantity(Integer value)) to throw a CoreException with an
appropriate error message when value is null or value < 0, and keep accepted
behavior for zero and positive values; then add unit tests (e.g.,
StockQuantityTest) covering null (expect CoreException), -1 (expect
CoreException), 0 (valid), and a positive value (valid) to verify the invariant.
| @Entity | ||
| @Table(name = "users") | ||
| public class User extends BaseEntity { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "User.java" -path "*/domain/user/*" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 253
🏁 Script executed:
fd -t f "BaseEntity.java" | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 145
🏁 Script executed:
find . -name "User.java" -path "*/domain/user/*"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 152
🏁 Script executed:
cat modules/jpa/src/main/java/com/loopers/domain/BaseEntity.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1780
🏁 Script executed:
cat ./apps/commerce-api/src/main/java/com/loopers/domain/user/User.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1408
equals/hashCode 구현이 누락되었다.
JPA 엔티티는 동일성을 올바르게 비교하기 위해 equals/hashCode가 필요하다. BaseEntity에 구현이 없으므로 User에서 반드시 구현해야 한다. 구현하지 않으면 Set, Map 컬렉션에서 동일 엔티티가 중복 저장되거나 영속성 컨텍스트에서 동일성 비교 오류가 발생한다.
수정안: User의 식별자인 BaseEntity의 id 필드를 기반으로 구현한다.
`@Override`
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return getId() != null && getId().equals(user.getId());
}
`@Override`
public int hashCode() {
return getId() != null ? getId().hashCode() : 0;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/commerce-api/src/main/java/com/loopers/domain/user/User.java` around
lines 10 - 12, Add equals and hashCode to the User entity to base identity on
BaseEntity's id: implement User.equals(Object) to return true for same instance,
false for non-User, and compare non-null getId() values for equality; implement
User.hashCode() to return getId().hashCode() when id is non-null or 0 when null.
Place these overrides in the User class alongside other methods to ensure
correct behavior in Sets/Maps and JPA persistence context.
| @PostMapping("/api/v1/products/{productId}/likes") | ||
| public ApiResponse<LikeV1Dto.LikeResponse> like( | ||
| @PathVariable Long productId, | ||
| @RequestHeader("X-Loopers-UserId") Long userId | ||
| ) { | ||
| LikeInfo info = likeFacade.like(userId, productId); | ||
| return ApiResponse.success(LikeV1Dto.LikeResponse.from(info)); | ||
| } | ||
|
|
||
| @DeleteMapping("/api/v1/products/{productId}/likes") | ||
| public ApiResponse<Object> unlike( | ||
| @PathVariable Long productId, | ||
| @RequestHeader("X-Loopers-UserId") Long userId | ||
| ) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) 사용자 식별 헤더 사용 지점 확인
echo "=== 1) X-Loopers-UserId 헤더 사용 현황 ==="
rg -n "X-Loopers-UserId|@RequestHeader.*UserId" apps/commerce-api/src/main/java | head -50
# 2) 서버 측 인증/식별 바인딩 컴포넌트 존재 여부 확인
echo -e "\n=== 2) 인증/보안 컴포넌트 확인 ==="
rg -n "SecurityContext|Authentication|@AuthenticationPrincipal|Principal|OncePerRequestFilter|HandlerMethodArgumentResolver" apps/commerce-api/src/main/java | head -50
# 3) 컨트롤러 인증 테스트 확인
echo -e "\n=== 3) 인증 관련 테스트 확인 ==="
fd '.*Test\.java' apps/commerce-api/src/test/java | xargs rg -l "401|403|Unauthorized|인증|X-Loopers-UserId|SecurityMockMvc" 2>/dev/null || echo "No auth tests found"
# 4) 보안 설정 파일 확인
echo -e "\n=== 4) 보안 설정 파일 확인 ==="
fd -e java -e xml -e yml | xargs grep -l "SecurityConfig\|WebSecurityConfig\|@EnableWebSecurity" 2>/dev/null | head -10 || echo "No security config found"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 582
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) 필터/인터셉터 구현 확인
echo "=== 1) Filter/Interceptor 구현 확인 ==="
rg -n "Filter|Interceptor" apps/commerce-api/src/main/java --type java | grep -E "(implements|extends)" | head -20
# 2) 커스텀 ArgumentResolver 확인
echo -e "\n=== 2) 커스텀 ArgumentResolver 확인 ==="
rg -n "HandlerMethodArgumentResolver|resolveArgument" apps/commerce-api/src/main/java --type java | head -20
# 3) 컨트롤러 파일 직접 확인 (LikeV1Controller 전체)
echo -e "\n=== 3) LikeV1Controller 전체 코드 확인 ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java
# 4) OrderV1Controller 비교 확인
echo -e "\n=== 4) OrderV1Controller 사용자 식별 부분 ==="
sed -n '20,40p' apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java
# 5) WebConfig 또는 SecurityConfig 찾기
echo -e "\n=== 5) 보안 설정/구성 파일 ==="
find apps/commerce-api/src -name "*Config.java" -o -name "*Configuration.java" | xargs ls -la 2>/dev/null
# 6) 테스트 디렉토리 구조 확인
echo -e "\n=== 6) 테스트 파일 존재 여부 ==="
find apps/commerce-api/src/test -name "*LikeV1ControllerTest*" -o -name "*LikeTest*" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 120
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) LikeV1Controller 전체 파일 읽기
echo "=== LikeV1Controller 전체 ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java
# 2) OrderV1Controller 전체 파일 읽기
echo -e "\n=== OrderV1Controller 전체 ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java
# 3) 프로젝트 구조 확인
echo -e "\n=== 프로젝트 주요 파일 ==="
ls -la apps/commerce-api/src/main/java/com/loopers/
# 4) 모든 자바 파일 목록
echo -e "\n=== 자바 파일 목록 ==="
find apps/commerce-api/src/main/java -name "*.java" | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5963
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) 프로젝트 Spring Boot 버전 및 보안 설정 확인
echo "=== pom.xml 또는 build.gradle 확인 ==="
find apps/commerce-api -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" | head -5
# 2) application.properties/yml 확인
echo -e "\n=== 애플리케이션 설정 파일 ==="
find apps/commerce-api/src/main/resources -type f
# 3) ApiResponse 클래스 확인
echo -e "\n=== ApiResponse 구조 ==="
find apps/commerce-api/src -name "ApiResponse.java"
</script>Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 415
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) build.gradle.kts 읽기
echo "=== build.gradle.kts ==="
cat apps/commerce-api/build.gradle.kts
# 2) application.yml 읽기
echo -e "\n=== application.yml ==="
cat apps/commerce-api/src/main/resources/application.yml
# 3) ApiResponse.java 읽기
echo -e "\n=== ApiResponse.java ==="
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiResponse.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2998
요청 헤더 기반 사용자 식별은 위변조 위험이 있다.
Line 22~35의 like(), unlike() 메서드와 OrderV1Controller의 createOrder() 메서드에서 @RequestHeader("X-Loopers-UserId")로 사용자를 식별하는데, 클라이언트가 제어하는 헤더 값에 의존하면 운영 환경에서 헤더 스푸핑으로 타인 계정 좋아요/취소가 가능해질 수 있다. Spring Security 의존성이나 인증 인프라가 없어 헤더 검증을 보호하는 upstream 필터나 인터셉터가 보이지 않는다.
인증된 주체에서 userId를 주입받도록 수정해야 한다. Spring Security를 도입하고 @AuthenticationPrincipal로 현재 사용자를 바인딩하거나, 서버 측 검증된 컨텍스트(예: ThreadLocal 기반 RequestContextHolder)를 통해 외부 헤더를 신뢰하지 않도록 경계를 명확히 해야 한다.
추가로 위조 헤더 요청 차단(401/403), 인증 주체와 헤더 불일치 시 거부 시나리오를 컨트롤러/통합 테스트에 포함해야 한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java`
around lines 22 - 35, The controller methods like() and unlike() (and likewise
OrderV1Controller.createOrder()) currently trust a client-provided
X-Loopers-UserId header—replace that with a server-trusted authenticated
principal: remove the `@RequestHeader`("X-Loopers-UserId") Long userId parameter
and instead inject the current user from the security context (e.g.
`@AuthenticationPrincipal` or a RequestContextHolder/ThreadLocal-backed
authenticated user provider) and use that ID for operations via likeFacade;
additionally enforce and return 401/403 when no authenticated principal is
present and add controller/integration tests that assert header-based spoofing
is rejected and that operations require the authenticated user's ID (and that
any header/user mismatch is denied).
| public record CreateRequest( | ||
| @NotEmpty List<@Valid OrderItemRequest> items | ||
| ) {} | ||
|
|
||
| public record OrderItemRequest( | ||
| @NotNull Long productId, | ||
| @NotNull @Min(1) Integer quantity | ||
| ) { | ||
| public OrderFacade.OrderItemRequest toFacadeRequest() { | ||
| return new OrderFacade.OrderItemRequest(productId, quantity); | ||
| } | ||
| } |
There was a problem hiding this comment.
Bean Validation 어노테이션 사용이 프로젝트 패턴과 불일치한다.
이 프로젝트는 DTO에서 Bean Validation을 사용하지 않고, 도메인 레이어에서 검증을 수행하는 패턴을 따른다. @NotEmpty, @NotNull, @Min 등의 어노테이션 사용은 MethodArgumentNotValidException을 발생시켜 CoreException 기반 통합 에러 처리 패턴과 충돌한다.
운영 관점에서 에러 응답 포맷 불일치가 발생할 수 있으며, 클라이언트가 두 가지 다른 에러 형식을 처리해야 한다. OrderItem 생성자에서 이미 수량 검증(quantity < 1)이 수행되므로 중복 검증이기도 하다.
🛠️ 수정안: Bean Validation 제거
-import jakarta.validation.Valid;
-import jakarta.validation.constraints.Min;
-import jakarta.validation.constraints.NotEmpty;
-import jakarta.validation.constraints.NotNull;
-
import java.util.List;
public class OrderV1Dto {
public record CreateRequest(
- `@NotEmpty` List<@Valid OrderItemRequest> items
+ List<OrderItemRequest> items
) {}
public record OrderItemRequest(
- `@NotNull` Long productId,
- `@NotNull` `@Min`(1) Integer quantity
+ Long productId,
+ Integer quantity
) {Based on learnings: "Do not use Bean Validation annotations (e.g., Valid, NotBlank, Email) on DTOs in this project. Move validation logic into the domain layer."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public record CreateRequest( | |
| @NotEmpty List<@Valid OrderItemRequest> items | |
| ) {} | |
| public record OrderItemRequest( | |
| @NotNull Long productId, | |
| @NotNull @Min(1) Integer quantity | |
| ) { | |
| public OrderFacade.OrderItemRequest toFacadeRequest() { | |
| return new OrderFacade.OrderItemRequest(productId, quantity); | |
| } | |
| } | |
| public record CreateRequest( | |
| List<OrderItemRequest> items | |
| ) {} | |
| public record OrderItemRequest( | |
| Long productId, | |
| Integer quantity | |
| ) { | |
| public OrderFacade.OrderItemRequest toFacadeRequest() { | |
| return new OrderFacade.OrderItemRequest(productId, quantity); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java`
around lines 14 - 25, Remove all Bean Validation annotations from the DTOs in
OrderV1Dto: delete `@NotEmpty` on CreateRequest.items, remove `@Valid` on the List
element type, and remove `@NotNull` and `@Min`(1) from OrderItemRequest fields
(productId and quantity). Keep the record definitions and the conversion helper
OrderItemRequest.toFacadeRequest() unchanged so domain-side validation (e.g.,
OrderItem constructor checks for quantity < 1) handles errors; also remove any
now-unused validation imports in this file.
| @Override | ||
| public User save(User user) { | ||
| return user; | ||
| } |
There was a problem hiding this comment.
save 메서드가 실제로 저장하지 않아 테스트 신뢰성이 저하된다.
save 메서드가 전달받은 User를 store에 저장하지 않고 그대로 반환한다. 프로덕션에서는 save 후 findById로 조회가 가능하지만, 이 Fake 구현에서는 불가능하다. 테스트가 잘못된 가정 하에 통과할 수 있어 운영 장애로 이어질 수 있다.
🐛 수정 제안
`@Override`
public User save(User user) {
+ // User의 ID가 null일 수 있으므로, ID 기반 저장 로직 필요
+ // BaseEntity에서 ID를 가져올 수 없다면 별도 시퀀스 사용 검토
+ // 최소한 ID가 있는 경우 저장하도록 수정
+ if (user != null && user.getId() != null) {
+ store.put(user.getId(), user);
+ }
return user;
}또는 User의 ID 접근이 어렵다면, addUserWithId를 통해서만 데이터를 시드하고 save는 업데이트 용도로만 사용하도록 테스트 전략을 명확히 문서화해야 한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/test/java/com/loopers/application/order/FakeUserRepository.java`
around lines 23 - 26, FakeUserRepository의 save(User user)는 현재 store에 저장하지 않고
입력값을 그대로 반환하므로 테스트 신뢰성을 해칩니다; 수정하려면 FakeUserRepository.save 메서드가 전달받은 User를 내부
store(예: Map 또는 List)에 저장하거나 업데이트하도록 구현하고 기존 findById가 해당 store에서 조회할 수 있게 하세요;
만약 User.id가 null이면 간단한 ID 생성 로직을 추가하거나 IllegalArgumentException을 던지도록 결정하고,
대안으로는 addUserWithId를 통한 시드 전용 정책을 문서화하여 save를 업데이트 전용으로만 사용하도록 명확히 표기하세요.
- OrderService의 @transactional 제거 (Facade에서만 트랜잭션 관리) - Product, User 변경 후 명시적 save 호출로 dirty checking 의존 제거 - ProductRepository에 save 메서드 추가 및 테스트용 FakeRepository 반영
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (2)
12-18: OrderItem을 개별 저장하는 N+1 쓰기 패턴이 존재한다.주문 항목을 루프 내에서 개별적으로
saveItem()을 호출하면 항목 개수만큼 INSERT 쿼리가 발생한다. 대량 주문 시 DB 부하가 증가하고 트랜잭션 시간이 길어질 수 있다.수정안:
OrderRepository에saveAllItems(List<OrderItem>)메서드를 추가하여 배치 저장을 수행하거나, JPA의saveAll()메서드를 활용하라.테스트: 다수의 주문 항목(예: 50개 이상)을 포함한 주문 생성 시 쿼리 수와 응답 시간을 측정하는 성능 테스트를 추가하라.
♻️ 개선 제안
public Order createOrder(Order order) { Order savedOrder = orderRepository.save(order); - for (OrderItem item : order.getOrderItems()) { - item.assignOrderId(savedOrder.getId()); - orderRepository.saveItem(item); - } + order.getOrderItems().forEach(item -> item.assignOrderId(savedOrder.getId())); + orderRepository.saveAllItems(order.getOrderItems()); return savedOrder; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java` around lines 12 - 18, The current createOrder in OrderService causes N+1 writes by calling orderRepository.saveItem(...) inside a loop; change it to save the Order first (OrderService.createOrder and savedOrder), set each OrderItem's orderId via OrderItem.assignOrderId(savedOrder.getId()), collect the items from Order.getOrderItems() and call a batch method on the repository (add OrderRepository.saveAllItems(List<OrderItem>) or use JPA's saveAll(List<OrderItem>)) so all items are persisted in one operation instead of per-item saves, and add a performance test that creates an order with many items (e.g., 50+) to assert reduced query count and acceptable response time.
14-18: 입력 객체의 하위 요소를 변경하는 부수 효과가 존재한다.
order.getOrderItems()에서 가져온OrderItem객체들의orderId를 변경하고 있다. 이는 호출자가 전달한Order객체의 상태를 암묵적으로 변경하는 부수 효과다. 반환된savedOrder와 실제로 저장된 항목들의 상태가 일치하지 않을 수 있어 디버깅 시 혼란을 야기한다.수정안: 메서드 시그니처 또는 JavaDoc에 이 부수 효과를 명시하거나, Order 엔티티 내부에서 orderId 할당 책임을 캡슐화하라.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java` around lines 14 - 18, The loop in OrderService that calls order.getOrderItems() and mutates each OrderItem via OrderItem.assignOrderId(...) creates an implicit side effect on the caller's Order; either document this behavior in the method JavaDoc/signature or move/encapsulate the responsibility into the domain: add a method on Order (e.g., Order.assignOrderIdToItems(savedOrder.getId()) or Order.withAssignedOrderId(savedOrder.getId())) that sets orderId for its items (or returns new items) and call that from OrderService before orderRepository.saveItem(...); alternatively clone the items before mutating to avoid changing the caller's object, and ensure OrderItem.assignOrderId and orderRepository.saveItem usage is consistent with the chosen approach.apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)
56-56:OrderItemRequest레코드에 입력값 검증이 필요하다.
productId와quantity에 대한 검증이 Application 계층에서 이루어지지 않는다. 현재quantity가 null인 경우 line 37의product.decreaseStock(req.quantity())에서 NPE가 발생하여 500 에러가 반환된다. 도메인 계층(OrderItem 생성자와 Product.decreaseStock)에서 값 검증이 있으나, Application 계층에서 입력값을 먼저 검증하면 사용자에게 명확한 400 BAD_REQUEST 응답을 제공할 수 있다.운영 관점: 입력값 검증이 Application 계층에서 이루어져야 클라이언트가 400 BAD_REQUEST를 받게 되고, 로깅과 모니터링 관점에서도 입력 오류와 서버 오류를 구분할 수 있다.
제안:
OrderItemRequest에@NotNull,@Min등의 검증 애너테이션을 추가하거나,createOrder메서드 진입 시 명시적으로 입력값을 검증하는 로직을 추가한다.예시
public record OrderItemRequest( `@NotNull`(message = "상품 ID는 필수입니다") Long productId, `@NotNull`(message = "주문 수량은 필수입니다") `@Min`(value = 1, message = "주문 수량은 1 이상이어야 합니다") Integer quantity ) {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java` at line 56, OrderItemRequest currently allows nulls causing a NPE in createOrder when calling product.decreaseStock(req.quantity()); add application-layer validation for OrderItemRequest by annotating fields in the OrderItemRequest record (e.g., `@NotNull` on productId and quantity and `@Min`(1) on quantity with clear messages) or perform explicit null/range checks at the start of createOrder to throw a 400 BAD_REQUEST with a descriptive message; ensure the validation prevents null/invalid values before calling Product.decreaseStock or constructing OrderItem so clients receive 400 responses instead of server 500 errors.build.gradle.kts (1)
48-71: 버전 관리 일관성 문제:extra변수 설정 후 미사용
extra["testcontainers.version"] = "1.21.0"을 설정했으나, Line 70-71에서 해당 변수를 참조하지 않고"1.21.0"을 직접 하드코딩하고 있다.운영 관점에서 이는 다음 문제를 야기한다:
- 향후 버전 업그레이드 시 여러 위치를 수정해야 하며, 누락 시 버전 불일치로 인한 테스트 환경 불안정 발생 가능
modules/jpa/build.gradle.kts에서도 동일 버전이 하드코딩되어 있어 총 4곳에서 중복 관리 필요♻️ 변수 참조 방식으로 수정
- testImplementation("org.testcontainers:testcontainers:1.21.0") - testImplementation("org.testcontainers:junit-jupiter:1.21.0") + testImplementation("org.testcontainers:testcontainers:${extra["testcontainers.version"]}") + testImplementation("org.testcontainers:junit-jupiter:${extra["testcontainers.version"]}")추가로
modules/jpa/build.gradle.kts에서도rootProject.extra["testcontainers.version"]을 참조하도록 수정하여 중앙 집중식 버전 관리를 적용해야 한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@build.gradle.kts` around lines 48 - 71, The build file hardcodes "1.21.0" for Testcontainers despite setting extra["testcontainers.version"], so update the dependency declarations that currently use "org.testcontainers:testcontainers:1.21.0" and "org.testcontainers:junit-jupiter:1.21.0" to reference the central variable (e.g. use rootProject.extra["testcontainers.version"] cast to String or Kotlin string interpolation) instead of the literal; also update modules/jpa/build.gradle.kts to read the same rootProject.extra["testcontainers.version"] so all Testcontainers references use the single source of truth.modules/jpa/build.gradle.kts (1)
17-20: 루트 프로젝트의 버전 변수를 참조하여 중앙 집중식 관리 적용 필요루트
build.gradle.kts에서extra["testcontainers.version"] = "1.21.0"을 설정했으나, 이 모듈에서는 참조하지 않고 버전을 직접 하드코딩하고 있다.운영 관점에서 버전 업그레이드 시 이 파일의 수정이 누락되면 모듈 간 Testcontainers 버전 불일치가 발생하여 테스트 환경 불안정 및 디버깅 어려움을 야기할 수 있다.
♻️ 루트 프로젝트 변수 참조 방식으로 수정
- testImplementation("org.testcontainers:mysql:1.21.0") + testImplementation("org.testcontainers:mysql:${rootProject.extra["testcontainers.version"]}") testFixturesImplementation("org.springframework.boot:spring-boot-starter-data-jpa") - testFixturesImplementation("org.testcontainers:mysql:1.21.0") + testFixturesImplementation("org.testcontainers:mysql:${rootProject.extra["testcontainers.version"]}")단, 루트
build.gradle.kts에서도 해당 변수를 실제로 사용하도록 수정해야 일관성이 유지된다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/jpa/build.gradle.kts` around lines 17 - 20, Replace the hardcoded Testcontainers version used in the dependency declarations (the two occurrences of org.testcontainers:mysql:1.21.0 in modules/jpa/build.gradle.kts) with the centralized version from the root project extra property ("testcontainers.version"); read the rootProject extra value into a local variable (or reference it) and interpolate that variable into the dependency coordinates so both testImplementation and testFixturesImplementation use the same rootProject.extra["testcontainers.version"] value; also ensure the root build.gradle.kts actually defines extra["testcontainers.version"] so the reference resolves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`:
- Around line 28-47: In createOrder, guard against empty or duplicate
itemRequests: first throw a CoreException(ErrorType.INVALID_REQUEST, "주문 항목이
비어있습니다") if itemRequests is empty; then collapse duplicate OrderItemRequest
entries by grouping on productId and summing quantities (so you only call
product.decreaseStock once per productId) before fetching Product, calling
product.decreaseStock, productRepository.save, and constructing OrderItem; add
unit tests covering an empty request and requests with duplicate productId to
verify aggregation and stock updates.
In `@build.gradle.kts`:
- Line 48: The Testcontainers version set via extra["testcontainers.version"] =
"1.21.0" pulls a vulnerable transitive dependency
(org.apache.commons:commons-compress 1.24.0); update this by either bumping
Testcontainers to a safe release (set extra["testcontainers.version"] = "1.21.4"
or "2.0.3") or by forcing commons-compress to >=1.26.0 in the Gradle
configuration (add a constraints or dependencyResolution force for
org.apache.commons:commons-compress:1.26.0+), then run the build and test suite
to verify compatibility.
---
Nitpick comments:
In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`:
- Line 56: OrderItemRequest currently allows nulls causing a NPE in createOrder
when calling product.decreaseStock(req.quantity()); add application-layer
validation for OrderItemRequest by annotating fields in the OrderItemRequest
record (e.g., `@NotNull` on productId and quantity and `@Min`(1) on quantity with
clear messages) or perform explicit null/range checks at the start of
createOrder to throw a 400 BAD_REQUEST with a descriptive message; ensure the
validation prevents null/invalid values before calling Product.decreaseStock or
constructing OrderItem so clients receive 400 responses instead of server 500
errors.
In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java`:
- Around line 12-18: The current createOrder in OrderService causes N+1 writes
by calling orderRepository.saveItem(...) inside a loop; change it to save the
Order first (OrderService.createOrder and savedOrder), set each OrderItem's
orderId via OrderItem.assignOrderId(savedOrder.getId()), collect the items from
Order.getOrderItems() and call a batch method on the repository (add
OrderRepository.saveAllItems(List<OrderItem>) or use JPA's
saveAll(List<OrderItem>)) so all items are persisted in one operation instead of
per-item saves, and add a performance test that creates an order with many items
(e.g., 50+) to assert reduced query count and acceptable response time.
- Around line 14-18: The loop in OrderService that calls order.getOrderItems()
and mutates each OrderItem via OrderItem.assignOrderId(...) creates an implicit
side effect on the caller's Order; either document this behavior in the method
JavaDoc/signature or move/encapsulate the responsibility into the domain: add a
method on Order (e.g., Order.assignOrderIdToItems(savedOrder.getId()) or
Order.withAssignedOrderId(savedOrder.getId())) that sets orderId for its items
(or returns new items) and call that from OrderService before
orderRepository.saveItem(...); alternatively clone the items before mutating to
avoid changing the caller's object, and ensure OrderItem.assignOrderId and
orderRepository.saveItem usage is consistent with the chosen approach.
In `@build.gradle.kts`:
- Around line 48-71: The build file hardcodes "1.21.0" for Testcontainers
despite setting extra["testcontainers.version"], so update the dependency
declarations that currently use "org.testcontainers:testcontainers:1.21.0" and
"org.testcontainers:junit-jupiter:1.21.0" to reference the central variable
(e.g. use rootProject.extra["testcontainers.version"] cast to String or Kotlin
string interpolation) instead of the literal; also update
modules/jpa/build.gradle.kts to read the same
rootProject.extra["testcontainers.version"] so all Testcontainers references use
the single source of truth.
In `@modules/jpa/build.gradle.kts`:
- Around line 17-20: Replace the hardcoded Testcontainers version used in the
dependency declarations (the two occurrences of org.testcontainers:mysql:1.21.0
in modules/jpa/build.gradle.kts) with the centralized version from the root
project extra property ("testcontainers.version"); read the rootProject extra
value into a local variable (or reference it) and interpolate that variable into
the dependency coordinates so both testImplementation and
testFixturesImplementation use the same
rootProject.extra["testcontainers.version"] value; also ensure the root
build.gradle.kts actually defines extra["testcontainers.version"] so the
reference resolves.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.javaapps/commerce-api/src/test/java/com/loopers/application/order/FakeProductRepository.javabuild.gradle.ktsmodules/jpa/build.gradle.kts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/commerce-api/src/test/java/com/loopers/application/order/FakeProductRepository.java
| public OrderInfo createOrder(Long userId, List<OrderItemRequest> itemRequests) { | ||
| User user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자를 찾을 수 없습니다")); | ||
|
|
||
| List<OrderItem> orderItems = new ArrayList<>(); | ||
| for (OrderItemRequest req : itemRequests) { | ||
| Product product = productRepository.findById(req.productId()) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다")); | ||
|
|
||
| product.decreaseStock(req.quantity()); | ||
| productRepository.save(product); | ||
|
|
||
| OrderItem orderItem = new OrderItem( | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getPrice(), | ||
| req.quantity() | ||
| ); | ||
| orderItems.add(orderItem); | ||
| } |
There was a problem hiding this comment.
주문 항목 목록에 대한 방어적 검증이 누락되었다.
itemRequests가 빈 리스트인 경우 주문 항목 없이 주문이 생성된다. 또한 동일 productId가 중복 포함된 경우 재고 차감이 여러 번 발생하여 의도치 않은 결과가 발생할 수 있다.
운영 관점: 빈 주문 생성은 데이터 정합성 문제를 야기하고, 중복 상품 ID는 재고 부정확을 초래한다.
수정안:
itemRequests가 비어있으면 예외를 발생시켜라.- 동일
productId항목은 수량을 합산하거나 예외를 발생시켜라.
테스트: 빈 주문 요청, 중복 상품 ID 요청에 대한 단위 테스트를 추가하라.
🛡️ 검증 로직 추가 제안
`@Transactional`
public OrderInfo createOrder(Long userId, List<OrderItemRequest> itemRequests) {
+ if (itemRequests == null || itemRequests.isEmpty()) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "주문 항목이 비어있습니다");
+ }
+
+ Set<Long> productIds = new HashSet<>();
+ for (OrderItemRequest req : itemRequests) {
+ if (!productIds.add(req.productId())) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "중복된 상품이 포함되어 있습니다");
+ }
+ }
+
User user = userRepository.findById(userId)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`
around lines 28 - 47, In createOrder, guard against empty or duplicate
itemRequests: first throw a CoreException(ErrorType.INVALID_REQUEST, "주문 항목이
비어있습니다") if itemRequests is empty; then collapse duplicate OrderItemRequest
entries by grouping on productId and summing quantities (so you only call
product.decreaseStock once per productId) before fetching Product, calling
product.decreaseStock, productRepository.save, and constructing OrderItem; add
unit tests covering an empty request and requests with duplicate productId to
verify aggregation and stock updates.
| } | ||
| } | ||
|
|
||
| extra["testcontainers.version"] = "1.21.0" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Testcontainers 1.21.0 Maven Central version
💡 Result:
Testcontainers 1.21.0 is available on Maven Central as:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.21.0</version>
</dependency>[1]
If you use the BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.21.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>[2]
Sources: [1] (repo1.maven.org) [2] (repo.maven.apache.org)
Citations:
- 1: https://repo1.maven.org/maven2/org/testcontainers/testcontainers/1.21.0/?utm_source=openai
- 2: https://repo.maven.apache.org/maven2/org/testcontainers/testcontainers-bom/?utm_source=openai
🌐 Web query:
Testcontainers 1.21.0 security vulnerabilities CVE
💡 Result:
Testcontainers 1.21.0 (the org.testcontainers:testcontainers artifact) has no known direct CVEs, but it brings in a vulnerable dependency: org.apache.commons:commons-compress:1.24.0, which is affected by these CVEs: (mvnrepository.com)
- CVE-2024-25710 – Apache Commons Compress DoS (infinite loop when reading a corrupted DUMP file). Affects 1.3–1.25.0; fixed in 1.26.0. (commons.apache.org)
- CVE-2024-26308 – Apache Commons Compress DoS (resource exhaustion /
OutOfMemoryErrorunpacking broken Pack200). Affects 1.21–<1.26.0; fixed in 1.26.0. (commons.apache.org)
Remediation options
- Override/force
commons-compressto ≥ 1.26.0 (preferably latest). (commons.apache.org) - Upgrade Testcontainers to 2.0.3, which depends on
commons-compress:1.28.0(not in the affected range). (mvnrepository.com)
Citations:
- 1: https://mvnrepository.com/artifact/org.testcontainers/testcontainers/1.21.0/dependencies
- 2: https://commons.apache.org/compress/security.html?utm_source=openai
- 3: https://commons.apache.org/compress/security.html?utm_source=openai
- 4: https://commons.apache.org/compress/security.html?utm_source=openai
- 5: https://mvnrepository.com/artifact/org.testcontainers/testcontainers/2.0.3/dependencies
🌐 Web query:
Testcontainers org.testcontainers:testcontainers versions 2024 2025
💡 Result:
org.testcontainers:testcontainers — versions released in 2024 and 2025
2024
- 1.19.4 (Jan 25, 2024) [1]
- 1.19.5 (Feb 08, 2024) [1]
- 1.19.6 (Feb 22, 2024) [1]
- 1.19.7 (Mar 05, 2024) [1]
- 1.19.8 (May 09, 2024) [1]
- 1.20.0 (Jul 17, 2024) [2]
- 1.20.1 (Jul 31, 2024) [2]
- 1.20.2 (Sep 30, 2024) [2]
- 1.20.3 (Oct 22, 2024) [2]
- 1.20.4 (Nov 20, 2024) [2]
2025
- 1.20.5 (Feb 19, 2025) [2]
- 1.20.6 (Mar 04, 2025) [2]
- 1.21.0 (Apr 23, 2025) [3]
- 1.21.1 (May 29, 2025) [4]
- 1.21.2 (Jun 18, 2025) [2]
- 1.21.3 (Jun 28, 2025) [2]
- 1.21.4 (Dec 16, 2025) [5]
- 2.0.1 (Oct 16, 2025) [2]
- 2.0.2 (Nov 13, 2025) [2]
- 2.0.3 (Dec 15, 2025) [2]
Sources: Maven Repository version listings / artifact pages [1] [2] and Maven Central directory timestamps [5].
Testcontainers 1.21.0의 전이 의존성(commons-compress) 보안 취약점 해결 필요
Testcontainers 1.21.0은 Maven Central에 유효한 버전으로 존재하나, 전이 의존성인 org.apache.commons:commons-compress:1.24.0에서 두 가지 DoS 취약점이 발견되었다.
보안 문제
- CVE-2024-25710: 손상된 DUMP 파일 읽기 시 무한 루프 발생
- CVE-2024-26308: 손상된 Pack200 압축 해제 시 메모리 고갈(OutOfMemoryError) 발생
- 두 취약점 모두 commons-compress 1.21~1.25.0 범위에 영향
운영상 영향
공격자가 악의적으로 구성된 압축 파일을 제공할 경우 테스트 환경의 컨테이너 리소스가 고갈되어 CI/CD 파이프라인 장애가 발생할 수 있다.
수정안
다음 중 하나를 선택하여 조치한다:
- commons-compress를 명시적으로 1.26.0 이상으로 강제 변경
- Testcontainers를 2.0.3 이상으로 업그레이드(commons-compress 1.28.0 포함)
최신 버전(1.21.4 또는 2.0.3)으로 업그레이드 후 빌드 및 테스트 실행을 통해 의존성 호환성을 검증한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@build.gradle.kts` at line 48, The Testcontainers version set via
extra["testcontainers.version"] = "1.21.0" pulls a vulnerable transitive
dependency (org.apache.commons:commons-compress 1.24.0); update this by either
bumping Testcontainers to a safe release (set extra["testcontainers.version"] =
"1.21.4" or "2.0.3") or by forcing commons-compress to >=1.26.0 in the Gradle
configuration (add a constraints or dependencyResolution force for
org.apache.commons:commons-compress:1.26.0+), then run the build and test suite
to verify compatibility.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java`:
- Line 12: 브랜드 이름 유니크 제약이 JPA 애노테이션에만 의존하고 있어 프로덕션(DB ddl-auto: none)에서 적용되지
않으므로, Flyway/Liquibase 마이그레이션에 brands 테이블의 name 컬럼에 대한 유니크 인덱스(또는 UNIQUE
CONSTRAINT)를 명시적으로 추가하고, 통합 테스트를 작성해 실제 DB에서 중복 이름 삽입 시 제약 위반 예외가 발생하는지 확인하도록
수정하세요; 관련 식별자: Brand 엔티티의 name 컬럼, BrandJpaRepository.existsByName, 기존 단위테스트
BrandServiceUnitTest(대신 DB 연동 통합테스트로 대체) 및 동시성 통합테스트를 추가해 동시 생성 시 하나만 성공함을
검증하세요.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/jpa/converter/BrandNameConverter.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/jpa/converter/StockQuantityConverter.javaapps/commerce-api/src/test/resources/docker-java.properties
✅ Files skipped from review due to trivial changes (1)
- apps/commerce-api/src/test/resources/docker-java.properties
|
|
||
| public interface BrandJpaRepository extends JpaRepository<Brand, Long> { | ||
|
|
||
| boolean existsByName(BrandName name); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Brand 엔티티와 마이그레이션 파일 위치 확인
find . -type f -name "*.java" | grep -i brand | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 996
🏁 Script executed:
# Brand 엔티티 정의 확인
fd 'Brand.java' | xargs -r cat -nRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 773
🏁 Script executed:
# 마이그레이션/스키마 파일 검색
find . -type f \( -name "*.sql" -o -name "*.xml" -o -name "*.yml" -o -name "*.yaml" \) | xargs -r grep -l -i "brand" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# BrandJpaRepository 파일 전체 내용 확인
cat -n "apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 532
🏁 Script executed:
# 마이그레이션 파일 디렉토리 검색
find . -type d -name "migration*" -o -name "flyway" -o -name "liquibase" | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Flyway 마이그레이션 파일 검색
find . -type f -name "V*.sql" -o -name "V*.java" | grep -i migration | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 프로젝트 루트의 db/migration 디렉토리 확인
ls -la apps/commerce-api/src/main/resources/db/migration/ 2>/dev/null || echo "Not found in default location"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 114
🏁 Script executed:
# 전체 SQL 파일 검색
find apps/commerce-api -name "*.sql" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Brand 관련 테스트 파일에서 동시성/중복 테스트 확인
rg -n "concurr|duplicate|同時|동시" --type java apps/commerce-api/src/test/java/com/loopers/domain/brand/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 전체 테스트 파일 목록 확인
find apps/commerce-api/src/test -name "*Brand*Test*.java" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 323
🏁 Script executed:
# 프로젝트 전체 SQL 파일 검색
find . -name "*.sql" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# application.properties/yml에서 JPA 설정 확인 (ddl-auto 설정)
find apps/commerce-api/src -name "application*.yml" -o -name "application*.properties" | xargs catRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1044
🏁 Script executed:
# BrandService에서 existsByName 사용 패턴 확인
cat -n apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 896
🏁 Script executed:
# jpa.yml 파일 확인 (ddl-auto 설정)
find apps/commerce-api -name "jpa.yml" -type f | xargs catRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# BrandServiceUnitTest 확인 (중복 생성 시나리오)
cat -n apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceUnitTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2071
🏁 Script executed:
# BaseEntity 확인 (ID 생성 전략)
cat -n apps/commerce-api/src/main/java/com/loopers/domain/BaseEntity.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 183
🏁 Script executed:
# BaseEntity 찾기 (다른 경로)
find apps/commerce-api -name "BaseEntity.java" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# jpa.yml 전체 위치 확인
find . -name "jpa.yml" -type f | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 125
🏁 Script executed:
# 통합 테스트 파일 찾기 (IT 또는 Integration 패턴)
find apps/commerce-api/src/test -name "*IT.java" -o -name "*IntegrationTest.java" | grep -i brandRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Brand 관련 모든 테스트 파일 내용 확인
cat -n apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandUnitTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1461
🏁 Script executed:
# jpa.yml 확인 (DDL 설정)
cat -n ./modules/jpa/src/main/resources/jpa.ymlRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2903
🏁 Script executed:
# FakeBrandRepository 구현 확인
cat -n apps/commerce-api/src/test/java/com/loopers/domain/brand/FakeBrandRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1154
existsByName 경로의 유니크 제약 선언은 있으나 운영 환경 스키마 생성 전략을 점검해야 한다.
Brand 엔티티에 @Column(unique = true) 선언이 있지만, 프로덕션 환경에서 ddl-auto: none으로 설정되어 있어 명시적 마이그레이션 없이는 데이터베이스에 유니크 제약이 생성되지 않는다. 현재는 JPA 애노테이션에만 의존하고 있으므로 Flyway/Liquibase 마이그레이션 스크립트로 brands 테이블의 name 컬럼에 유니크 인덱스를 반드시 정의해야 한다.
기존 BrandServiceUnitTest는 단위 테스트로 FakeBrandRepository를 사용하므로 실제 데이터베이스 제약을 검증하지 못한다. 다음 추가 작업이 필요하다:
- Flyway 마이그레이션 파일에서 유니크 인덱스 명시적 생성
- 통합 테스트로 데이터베이스 유니크 제약 동작 검증
- 동시성 통합 테스트로 동일 이름 동시 생성 시 1건만 성공하고 나머지는 예외 발생 확인
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java`
at line 12, 브랜드 이름 유니크 제약이 JPA 애노테이션에만 의존하고 있어 프로덕션(DB ddl-auto: none)에서 적용되지
않으므로, Flyway/Liquibase 마이그레이션에 brands 테이블의 name 컬럼에 대한 유니크 인덱스(또는 UNIQUE
CONSTRAINT)를 명시적으로 추가하고, 통합 테스트를 작성해 실제 DB에서 중복 이름 삽입 시 제약 위반 예외가 발생하는지 확인하도록
수정하세요; 관련 식별자: Brand 엔티티의 name 컬럼, BrandJpaRepository.existsByName, 기존 단위테스트
BrandServiceUnitTest(대신 DB 연동 통합테스트로 대체) 및 동시성 통합테스트를 추가해 동시 생성 시 하나만 성공함을
검증하세요.
📌 Summary
🏗️ Design Overview
이번 구현의 목표는 단순 기능 완성이 아니라
도메인 중심 설계와 계층 간 책임 분리를 명확히 하는 것이었습니다.
설계 시 다음 원칙을 기준으로 구조를 구성했습니다.
비즈니스 규칙은 가능한 한 Entity 내부에 배치하여
객체가 자신의 상태와 행위를 함께 관리하도록 설계했습니다.
예시:
Product.decreaseStock()→ 재고 부족 예외 처리User.deductPoint()→ 포인트 부족 예외 처리Order.calculateTotalPrice()→ 총 금액 계산 책임Application(Facade)은 여러 도메인을 조합하는 역할만 수행하도록 제한했습니다.
🧭 Context & Decision
1️⃣ 왜 4계층 구조를 유지했는가?
도메인 로직과 기술 구현을 분리하기 위함입니다.
이 구조를 유지하면,
도메인을 기술로부터 보호할 수 있다고 판단했습니다.
2️⃣ Repository를 Domain에 둔 이유
DIP를 적용하기 위해
Repository 인터페이스는 Domain에 위치시켰습니다.
이를 통해:
3️⃣ Order 유스케이스 설계 결정
Order 생성 시 흐름:
이 흐름은 여러 도메인을 조합하는 작업이기 때문에
Domain Service가 아닌 Application(Facade) 에 배치했습니다.
트랜잭션 경계 역시 유스케이스 단위로 묶는 것이 자연스럽다고 판단했습니다.
4️⃣ 연관관계 매핑에 대한 판단
현재 Order와 OrderItem은 단순 구조로 유지했습니다.
추후 조회 기능이 추가되면 연관관계 매핑을 보강할 예정입니다.
📌 Architecture Overview
본 프로젝트는 4계층 Layered Architecture를 기반으로 구성했습니다.
Domain은 외부 계층을 참조하지 않도록 설계하여 DIP를 유지했습니다.
📌 Layered Structure Diagram
flowchart TB subgraph Interfaces C1[LikeV1Controller] C2[OrderV1Controller] DTO[Request/Response DTO] end subgraph Application F1[LikeFacade] F2[OrderFacade] end subgraph Domain E1[Like] S1[LikeService] R1[LikeRepository] E2[Order / OrderItem] S2[OrderService] R2[OrderRepository] E3[Product] R3[ProductRepository] E4[User] R4[UserRepository] VO[Value Objects] end subgraph Infrastructure IMPL1[RepositoryImpl] JPA[JpaRepository] end C1 --> F1 C2 --> F2 F1 --> S1 F2 --> S2 S1 --> R1 S2 --> R2 IMPL1 --> R1 IMPL1 --> R2 IMPL1 --> R3 IMPL1 --> R4 IMPL1 --> JPA <!-- This is an auto-generated comment: release notes by coderabbit.ai --> 변경 목적 DDD 기반 4계층(interfaces/application/domain/infrastructure) 아키텍처를 적용하고 Brand/Product/Like/Order 중심 도메인 모델과 페이사드 유스케이스를 도입해 책임 분리와 DIP 준수를 목표로 합니다. 핵심 변경점 도메인 엔티티(Brand/BrandName, Product/StockQuantity/Price, User, Like, Order/OrderItem)와 도메인 규칙(재고 변경, 포인트 차감, 주문 총액 계산) 추가; 애플리케이션 계층에 LikeFacade·OrderFacade(유스케이스 조율 및 트랜잭션 경계) 추가; 인프라 계층에 JPA 리포지토리와 구현체(Brand/Like/Order/Product/User) 및 JPA AttributeConverter(BrandName, StockQuantity) 추가; REST 컨트롤러(LikeV1Controller, OrderV1Controller)와 여러 단위/통합 테스트 추가. 리스크/주의사항 트랜잭션 경계가 페이사드(유스케이스) 수준으로 집중되어 예외·롤백 정책과 영속성 동작(명시적 save vs JPA 더티체킹) 상호작용을 운영 환경에서 검증해야 합니다. Order와 OrderItem의 JPA 매핑을 현재 단순화(transient 후 별도 저장)해 조회·일관성·성능(조인/지연로딩) 설계 보완이 필요합니다. 또한 로컬 변경(명시적 save 호출)이 의도대로 동작하는지 통합 환경에서 확인이 권장됩니다. 테스트/검증 방법 이미 Brand/Product/Like 유닛 테스트와 LikeService 통합 테스트, OrderFacade 유닛 테스트가 추가되어 도메인 규칙과 페이사드 흐름을 검증했습니다. 권장: 주문 생성·영속성(orders/order_items 저장), User 포인트·Product 재고 반영 및 트랜잭션 롤백 시나리오를 포함한 통합/E2E 테스트를 추가로 실행해 주세요. (확인) 현재 Order 관련 통합/엔드투엔드 테스트가 별도로 존재하는지요? <!-- end of auto-generated comment: release notes by coderabbit.ai -->