Conversation
- 장바구니 최대 100개 제한 제거 - likes_count 동시성: @Version 낙관적 락 명시 (시퀀스 다이어그램, 클래스 다이어그램, 요구사항) - 주문번호 생성 전략: UUID 기반 (ORD-yyyyMMdd-{UUID 앞 8자리}) 명시 - 주문 취소 트랜잭션: @transactional 원자적 처리 범위 명시 - 물리적 FK: ORDERS→USERS, ORDER_ITEMS→ORDERS 구간 DDL FK 제약 명시 - OrderFacade 추가: OrderService + CartService 조율로 관심사 분리 - CartService 추가: 장바구니 관리 책임 분리 - OrderService.generateOrderNumber() 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 장바구니(Cart) 도메인 전체 제거 (요구사항, 시퀀스, 클래스, ERD) - User에 balance 필드 추가 (USERS 테이블 포함) - 주문 생성 시퀀스: 잔액 확인 → 잔액 차감 흐름 추가 - 주문 취소 시퀀스: 잔액 복구 단계 추가 (UserRepo 참여자 추가) - 기능 요구사항: 잔액 조회, 잔액 확인/차감/복구 항목 추가 - 시나리오 3 → 상품 주문으로 변경 (장바구니 시나리오 삭제) - 핵심 제약: 잔액 부족 시 주문 실패 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- User: balance 추가, deductBalance/restoreBalance, authenticate() - Brand: 등록/조회/삭제 CRUD - Product: @Version 낙관적 락, decreaseStock/increaseStock, likesCount - Like: 중복 방지(CONFLICT), 취소(NOT_FOUND), Product.likesCount 연동 - Order/OrderItem: UUID 주문번호, PENDING/CONFIRMED/CANCELLED 상태 관리 - OrderFacade: 재고 차감 + 잔액 차감 단일 트랜잭션, 취소 시 원복 - 각 도메인 단위 테스트 (LikeTest, LikeServiceTest, OrderTest, OrderServiceTest 등) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BrandFacade.deleteBrand에 @transactional 추가 (연쇄 삭제 원자성 보장) - OrderFacade.cancelOrder에 주문 소유자 검증 추가 (타인 주문 취소 시 NOT_FOUND) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cancelOrder(Long id) → cancelOrder(Order order)로 변경하여 중복 조회 제거 - Facade에서 조회한 Order를 직접 전달하도록 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UserServiceTest: isNotNull() 중복 제거 (isEqualTo로 충분) - OrderTest: 테스트 이름과 무관한 필드 검증 제거 (status만 검증) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UserFacade 생성: getMyInfo 유스케이스를 application 레이어로 이동 - UserService에서 getMyInfo 제거 및 UserInfo import 삭제 (domain → application 역방향 의존 해소) - UserV1Controller가 getMyInfo를 UserFacade를 통해 호출하도록 수정 - UserFacadeTest 추가, UserServiceTest의 GetMyInfo 테스트 이동 - UserV1ControllerStandaloneTest를 UserFacade mock 기반으로 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Service 레이어(User/Brand/Product/Like/Order/Member)의 @transactional 전부 제거 - BrandFacade: register(@transactional), getBrand/getBrands(@transactional readOnly) 추가 - ProductFacade: 모든 메서드에 @transactional 추가 - 트랜잭션 경계를 유스케이스 단위인 Facade로 일원화 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- User/Brand/Product/Like/Order/MemberService 메서드에 @transactional 복원 - 쓰기 메서드: @transactional, 읽기 메서드: @transactional(readOnly = true) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthrough브랜드, 상품, 주문, 좋아요, 사용자 도메인에 대한 엔티티, 저장소 인터페이스, 도메인 서비스, 애플리케이션 파사드, REST 컨트롤러 및 DTO를 추가하여 전자상거래 기능을 확장한다. 트랜잭션 관리, 소프트 삭제, 낙관적 잠금을 포함한 44개 파일과 11개 테스트 파일이 신규 추가된다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant OrderFacade as OrderFacade<br/>(Application)
participant UserService as UserService<br/>(Domain)
participant ProductService as ProductService<br/>(Domain)
participant BrandService as BrandService<br/>(Domain)
participant OrderService as OrderService<br/>(Domain)
Client->>OrderFacade: createOrder(loginId, password, items)
OrderFacade->>UserService: authenticate(loginId, password)
UserService-->>OrderFacade: User
OrderFacade->>ProductService: getProductsByIds(itemIds)
ProductService-->>OrderFacade: List<Product>
OrderFacade->>BrandService: getBrandsByIds(brandIds)
BrandService-->>OrderFacade: List<Brand>
OrderFacade->>UserService: deductBalance(totalAmount)
UserService-->>OrderFacade: void
OrderFacade->>ProductService: decreaseStock(productId, quantity)
ProductService-->>OrderFacade: void
OrderFacade->>OrderService: createOrder(userId, orderNumber, totalAmount)
OrderService-->>OrderFacade: Order
OrderFacade->>OrderService: createOrderItem(orderId, productInfo)
OrderService-->>OrderFacade: OrderItem
OrderFacade-->>Client: OrderInfo
sequenceDiagram
participant Client
participant LikeFacade as LikeFacade<br/>(Application)
participant UserService as UserService<br/>(Domain)
participant LikeService as LikeService<br/>(Domain)
participant ProductService as ProductService<br/>(Domain)
Client->>LikeFacade: addLike(loginId, password, productId)
LikeFacade->>UserService: authenticate(loginId, password)
UserService-->>LikeFacade: User
LikeFacade->>ProductService: getProduct(productId)
ProductService-->>LikeFacade: Product
LikeFacade->>LikeService: addLike(userId, productId)
LikeService-->>LikeFacade: Like
LikeFacade->>ProductService: increaseLikes(productId)
ProductService-->>LikeFacade: void
LikeFacade-->>Client: ApiResponse<Void>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55분 주문 생성 흐름에서 사용자 인증, 상품 재고 차감, 브랜드 정보 일괄 로드 등 다양한 도메인 서비스를 조율하는 복잡한 로직이 포함되어 있다. 또한 소프트 삭제, 낙관적 잠금, 트랜잭션 관리가 여러 계층에 걸쳐 있으며, 상태 전이 검증 로직이 있다. 44개 파일에 걸친 광범위한 변경으로 인해 각 계층 간 의존성과 데이터 일관성을 면밀히 검토해야 한다. 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 |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (6)
apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java-24-29 (1)
24-29:⚠️ Potential issue | 🟡 Minor브랜드명 정규화와 길이 검증이 없어 DB 예외로 지연 실패할 수 있다.
현재는 공백 패턴과 100자 초과 입력이 도메인에서 걸러지지 않아 운영 중 DB 예외로 늦게 실패하거나 동일 의미 데이터가 분산 저장될 수 있다. 생성자에서
trim()정규화 후 blank/최대 길이 검증을 추가해 도메인 계층에서 조기 실패시키는 것이 안전하다. 경계값 테스트로null," ",101자 문자열," 나이키 "입력 케이스를 추가해야 한다.변경 제안
public Brand(String name, String description) { - if (name == null || name.isBlank()) { + String normalizedName = (name == null) ? null : name.trim(); + if (normalizedName == null || normalizedName.isBlank()) { throw new CoreException(ErrorType.BAD_REQUEST, "브랜드 이름은 비어있을 수 없습니다."); } - this.name = name; + if (normalizedName.length() > 100) { + throw new CoreException(ErrorType.BAD_REQUEST, "브랜드 이름은 100자 이하여야 합니다."); + } + this.name = normalizedName; this.description = description; }As per coding guidelines
**/*.java: "예외 흐름이 명확한지 점검" 및**/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/Brand.java` around lines 24 - 29, The Brand constructor currently accepts names without normalization or max-length checks which can cause DB errors or duplicate semantic entries; in Brand(String name, String description) call name = name == null ? null : name.trim(), then validate: if name is null or name.isBlank() or name.length() > 100 throw new CoreException(ErrorType.BAD_REQUEST, "..."); update any usages relying on raw input and add unit tests for Brand constructor covering null, " ", a 101-character string, and a trimmed input like " 나이키 " to assert trimming and the length/blank validations.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java-20-24 (1)
20-24:⚠️ Potential issue | 🟡 Minor
toOrderItemRequests()에서items가 null일 경우 NullPointerException이 발생한다.클라이언트가
items필드 없이 요청을 보내면items가 null이 되어items.stream()호출 시 NPE가 발생한다. 운영 환경에서 잘못된 요청으로 인해 500 에러가 반환되어 사용자 경험이 저하될 수 있다.수정안: 도메인 레이어에서 검증하거나, 메서드 내에서 방어적으로 처리한다.
방어적 처리 예시
public List<OrderRequest.OrderItemRequest> toOrderItemRequests() { + if (items == null || items.isEmpty()) { + return List.of(); + } return items.stream() .map(i -> new OrderRequest.OrderItemRequest(i.productId(), i.quantity())) .toList(); }추가 테스트:
items가 null이거나 빈 리스트인 요청에 대해 적절한 에러 응답이 반환되는지 검증하는 테스트를 추가한다.🤖 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 20 - 24, toOrderItemRequests() can NPE when items is null; update the method (OrderV1Dto::toOrderItemRequests) to defensively handle a null items field by treating it as an empty list (e.g., check items == null and return an empty List or stream over Collections.emptyList()) before mapping to OrderRequest.OrderItemRequest, and add tests verifying behavior for null and empty items inputs so the API returns a controlled response rather than throwing a NullPointerException.apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java-37-40 (1)
37-40:⚠️ Potential issue | 🟡 Minor
findAllByIds에서 빈 리스트 입력 시 문제가 발생할 수 있다.빈
ids리스트가 전달될 경우WHERE id IN ()형태의 SQL이 생성되어 일부 DB에서 오류가 발생할 수 있다. 호출부(ProductService, LikeFacade 등)에서 방어 로직이 없다면 운영 장애로 이어질 수 있다.수정안: 빈 리스트일 경우 빈 결과를 즉시 반환한다.
제안하는 수정
`@Override` public List<Product> findAllByIds(List<Long> ids) { + if (ids == null || ids.isEmpty()) { + return List.of(); + } return productJpaRepository.findAllByIdInAndDeletedAtIsNull(ids); }🤖 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/product/ProductRepositoryImpl.java` around lines 37 - 40, ProductRepositoryImpl.findAllByIds should guard against an empty ids list to avoid generating a SQL "IN ()" error; if ids is null or empty, return Collections.emptyList() immediately instead of calling productJpaRepository.findAllByIdInAndDeletedAtIsNull(ids). Update the method to check (ids == null || ids.isEmpty()) and return an empty list, leaving callers like ProductService and LikeFacade unchanged.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java-53-62 (1)
53-62:⚠️ Potential issue | 🟡 Minor
OrderResponse.from()에서info.items()가 null일 경우 NPE가 발생할 수 있다.
info.items()가 null이면stream()호출 시 NPE가 발생한다. OrderInfo 생성 시점에 items가 항상 non-null임을 보장하거나 방어 코드를 추가해야 한다.방어적 처리 예시
public static OrderResponse from(OrderInfo info) { + List<OrderItemResponse> itemResponses = info.items() != null + ? info.items().stream().map(OrderItemResponse::from).toList() + : List.of(); return new OrderResponse( info.id(), info.userId(), info.orderNumber(), info.status(), info.totalAmount(), - info.items().stream().map(OrderItemResponse::from).toList() + itemResponses ); }🤖 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 53 - 62, OrderResponse.from currently calls info.items().stream() which can NPE if items is null; update OrderResponse.from to defensively handle null items by treating a null items collection as empty before streaming (e.g., check info.items() == null or use Optional/Collections.emptyList()), then map elements with OrderItemResponse.from and collect toList; keep the rest of the constructor arguments unchanged so OrderResponse.from and OrderItemResponse.from are used consistently.apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java-28-41 (1)
28-41:⚠️ Potential issue | 🟡 Minor
getLikedProducts에서 빈productIds리스트 처리가 누락되어 있다.
productIds가 빈 리스트일 경우productService.getProductsByIds(productIds)호출 시 빈 IN 절(WHERE id IN ())이 생성되어 DB 벤더에 따라 SQL 오류가 발생할 수 있다. 운영 환경에서 좋아요가 없는 사용자 조회 시 장애로 이어질 수 있다.수정안: 빈 리스트일 경우 조기 반환하여 불필요한 DB 호출을 방지한다.
제안하는 수정
`@Transactional`(readOnly = true) public List<ProductInfo> getLikedProducts(String loginId, String rawPassword) { User user = userService.authenticate(loginId, rawPassword); List<Long> productIds = likeService.getLikedProductIds(user.getId()); + if (productIds.isEmpty()) { + return List.of(); + } // 상품 목록을 IN 쿼리로 한 번에 조회 (N+1 방지) List<Product> products = productService.getProductsByIds(productIds);추가 테스트: 좋아요가 없는 사용자에 대해
getLikedProducts호출 시 빈 리스트가 반환되는지 검증하는 테스트를 추가한다.🤖 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/like/LikeFacade.java` around lines 28 - 41, getLikedProducts currently calls productService.getProductsByIds(productIds) even when productIds is empty, which can produce an invalid IN () SQL; modify getLikedProducts to check if productIds.isEmpty() after obtaining them and return Collections.emptyList() (or List.of()) immediately to avoid the DB call, and ensure downstream mapping (brandIds/brandMap) is skipped; also add a unit/integration test asserting that a user with no likes causes getLikedProducts(loginId, rawPassword) to return an empty list.apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java-27-34 (1)
27-34:⚠️ Potential issue | 🟡 Minor가격 검증 테스트가 음수 경계값을 실제로 검증하지 않는다
Line 27의 시나리오는 "0 이하"인데 현재 입력이
0하나뿐이라 음수 가격 검증이 깨져도 테스트가 통과한다.@ParameterizedTest로0과 음수를 함께 검증하도록 수정해야 회귀를 조기에 막을 수 있다. 추가로 동일 경계값을Product.update경로에도 테스트해 생성/수정 규칙 일관성을 검증해야 한다.As per coding guidelines `**/*Test*.java`: 단위 테스트는 경계값/실패 케이스/예외 흐름을 포함하는지 점검한다.수정 예시
+import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; ... - `@Test` - void throwsBadRequest_whenPriceIsZeroOrNegative() { + `@ParameterizedTest` + `@ValueSource`(ints = {0, -1}) + void throwsBadRequest_whenPriceIsZeroOrNegative(int invalidPrice) { CoreException exception = assertThrows(CoreException.class, - () -> new Product(1L, "나이키 신발", 0, 100, "설명", "url")); + () -> new Product(1L, "나이키 신발", invalidPrice, 100, "설명", "url"));🤖 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/ProductTest.java` around lines 27 - 34, The current test throwsBadRequest_whenPriceIsZeroOrNegative only checks 0 and misses negative boundaries; change it to a `@ParameterizedTest` in ProductTest to run both 0 and negative values (e.g., -1) against new Product(...) and assert CoreException with ErrorType.BAD_REQUEST, and add a matching parameterized test for Product.update(...) to ensure the same validation on updates; reference the Product constructor and Product.update, and replace the single `@Test` method throwsBadRequest_whenPriceIsZeroOrNegative with parameterized variants to cover both creation and update paths.
🧹 Nitpick comments (24)
apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java (4)
18-30: OrderItem 입력 검증 테스트 누락
OrderItem생성 테스트가 유효한 입력에 대해서만 검증하고 있다. 다음 예외 케이스에 대한 테스트가 필요하다:
productName이 null 또는 빈 문자열인 경우price가 0 이하인 경우quantity가 0 이하인 경우스냅샷 데이터의 무결성은 주문 이력 조회 및 정산에 직접적으로 영향을 미치므로, 잘못된 데이터가 저장되지 않도록 검증이 필수적이다.
🤖 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/OrderTest.java` around lines 18 - 30, Add unit tests for invalid OrderItem inputs: create new test methods alongside savesSnapshot_whenInputIsValid that verify OrderItem throws appropriate exceptions when productName is null or empty, when price <= 0, and when quantity <= 0; reference the OrderItem constructor in your tests and use assertions (e.g., assertThrows) to assert the expected exception type/message for each case so invalid snapshot data cannot be created.
74-84: 상태 전이 테스트 시나리오 확장 필요현재
approve테스트는 이미CONFIRMED상태인 주문을 다시 승인하는 경우만 검증한다.CANCELLED상태의 주문을 승인하려는 경우도 테스트해야 한다. 운영 환경에서 취소된 주문이 실수로 승인 처리되는 버그를 사전에 방지할 수 있다.♻️ CANCELLED 상태에서 승인 시도 테스트
`@DisplayName`("CANCELLED 상태의 주문을 승인하면, BAD_REQUEST 예외가 발생한다.") `@Test` void throwsBadRequest_whenOrderIsCancelled() { Order order = new Order(1L, "ORD-20240101-ABCD1234", 50000L); order.cancel(); CoreException exception = assertThrows(CoreException.class, order::approve); assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }🤖 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/OrderTest.java` around lines 74 - 84, Add a new test in OrderTest that verifies approving a CANCELLED order throws BAD_REQUEST: create an Order instance, call order.cancel(), then assertThrows(CoreException.class, order::approve) and assert that exception.getErrorType() equals ErrorType.BAD_REQUEST; name the test method throwsBadRequest_whenOrderIsCancelled and include a DisplayName describing the scenario to mirror the existing pending-state test structure.
104-114: 상태 전이 테스트 시나리오 확장 필요
cancel테스트도 마찬가지로CONFIRMED상태의 주문을 취소하려는 경우를 테스트해야 한다. 이미 확정된 주문의 취소 시도에 대한 예외 처리가 올바르게 동작하는지 검증이 필요하다.♻️ CONFIRMED 상태에서 취소 시도 테스트
`@DisplayName`("CONFIRMED 상태의 주문을 취소하면, BAD_REQUEST 예외가 발생한다.") `@Test` void throwsBadRequest_whenOrderIsConfirmed() { Order order = new Order(1L, "ORD-20240101-ABCD1234", 50000L); order.approve(); CoreException exception = assertThrows(CoreException.class, order::cancel); assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }🤖 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/OrderTest.java` around lines 104 - 114, Add a new unit test in OrderTest that mirrors the existing non-PENDING cancellation test but sets the order to CONFIRMED before calling cancel: create an Order instance, call order.approve(), then assert that calling order.cancel() throws a CoreException with ErrorType.BAD_REQUEST; reference the existing test structure and use the same assertThrows/assertThat pattern as in the throwsBadRequest_whenOrderIsNotPending test to keep consistency.
47-54: 경계값 테스트 보강 필요: 음수 totalAmount현재
totalAmount가 0인 경우만 테스트하고 있다. 음수 값에 대한 테스트도 추가하여 경계값 검증을 완전하게 해야 한다.♻️ 음수 totalAmount 테스트 추가 예시
`@DisplayName`("totalAmount가 음수이면, BAD_REQUEST 예외가 발생한다.") `@Test` void throwsBadRequest_whenTotalAmountIsNegative() { CoreException exception = assertThrows(CoreException.class, () -> new Order(1L, "ORD-20240101-ABCD1234", -1000L)); assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }🤖 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/OrderTest.java` around lines 47 - 54, Add a negative-value boundary test mirroring the existing zero-value test: create a new test method (e.g., throwsBadRequest_whenTotalAmountIsNegative) in OrderTest that constructs an Order with a negative totalAmount (e.g., -1000L) and asserts it throws CoreException with ErrorType.BAD_REQUEST; reference the same constructor used in the current test (new Order(1L, "ORD-20240101-ABCD1234", -1000L)) and use assertThrows/assertThat like the existing throwsBadRequest_whenTotalAmountIsNotPositive test.apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java (1)
3-4: 도메인 레이어의 Spring Data 의존성
Page와Pageable은org.springframework.data.domain패키지에 속하며, 도메인 레이어가 인프라스트럭처 프레임워크에 의존하게 된다. PR 설명에서 JPA 엔티티를 도메인 레이어에 두는 결정을 내렸으므로 현재 설계와 일관성이 있으나, 향후 프레임워크 교체 시 도메인 레이어 변경이 필요하다는 점을 인지해야 한다.🤖 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/ProductRepository.java` around lines 3 - 4, ProductRepository currently imports Spring Data types Page and Pageable, coupling the domain layer to framework APIs; to fix this, remove org.springframework.data.domain.Page/Pageable from the domain by introducing a domain-agnostic pagination contract (e.g., PaginationRequest and PaginationResult or PageDto) and update ProductRepository to use those types, or alternatively move ProductRepository out of the domain layer into the infrastructure layer and keep a domain-facing repository interface that uses pure domain types; reference the ProductRepository interface and replace usages of Page and Pageable throughout the codebase with the new pagination abstractions or adapters.apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java (1)
13-13: 대량 데이터 조회 시 메모리 이슈 가능성
findAllByDeletedAtIsNull()메서드는 페이지네이션 없이 모든 브랜드를 조회한다. 브랜드 수가 증가하면 메모리 부족 및 응답 지연이 발생할 수 있다.관리자 화면 등 전체 목록이 필요한 경우에도 페이지네이션을 적용하거나, 조회 용도에 따라
findAllByDeletedAtIsNull(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/infrastructure/brand/BrandJpaRepository.java` at line 13, The repository method findAllByDeletedAtIsNull() in BrandJpaRepository returns all rows without pagination which can cause OOM for large datasets; replace or overload it with a paginated variant by adding findAllByDeletedAtIsNull(Pageable pageable) and update call sites (e.g., admin listing endpoints or service methods) to accept a Pageable and request paged results, or limit batch sizes for bulk processing; ensure service/controller code uses the new Pageable-based method and adjust tests accordingly.apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java (1)
14-44: 경계값 및 추가 예외 케이스 테스트 누락현재 테스트는
null, 공백(" "), 유효한 이름에 대한 케이스만 검증한다. 테스트 가이드라인에 따라 다음 경계값/예외 케이스를 추가하는 것이 좋다:
- 빈 문자열
""- 공백(" ")과 다른 케이스로 별도 검증 필요- description이 null인 경우 - 허용 여부에 따른 동작 검증
- 이름 길이 경계값 - 최대 길이 초과 시 동작 검증 (DB 컬럼 제약과 일치 여부)
- 특수문자/유니코드 - 이름에 특수문자 포함 시 정상 처리 여부
운영 관점에서 이러한 경계값 테스트가 누락되면, 프로덕션에서 예상치 못한 입력에 대해 장애가 발생할 수 있다.
🤖 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/BrandTest.java` around lines 14 - 44, Add missing boundary and edge-case tests to BrandTest: add a test method throwsBadRequest_whenNameIsEmpty that constructs new Brand("") and asserts BAD_REQUEST; add createsBrand_whenDescriptionIsNull (or throws depending on domain rule) to construct new Brand("Valid", null) and assert expected behavior/error; add name length boundary tests (createsBrand_whenNameAtMaxLength and throwsBadRequest_whenNameExceedsMaxLength) that build names at the DB/validation max length and one longer and assert success or BAD_REQUEST accordingly; and add createsBrand_withSpecialCharactersAndUnicode to construct new Brand with special chars/unicode (e.g., "ブランド-№✓") and assert the Brand is created with correct name/description. Reference the Brand constructor and BrandTest/Create nested class when adding these test methods.apps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.java (1)
5-11: 불변성 보장을 위한 방어적 복사 검토 필요
items필드가List<OrderItemRequest>로 선언되어 외부에서 전달받은 리스트를 그대로 참조한다. 이로 인해 다음 문제가 발생할 수 있다:
- 외부 변경 가능성 - 호출자가 전달 후 리스트를 수정하면 DTO 내부 상태가 변경됨
- null 리스트/요소 -
items가 null이거나 null 요소 포함 시 후속 처리에서 NPE 발생운영 환경에서 예측 불가능한 상태 변경으로 인한 장애를 방지하려면 방어적 복사를 권장한다.
♻️ 방어적 복사 적용 예시
public record OrderRequest( List<OrderItemRequest> items ) { + public OrderRequest { + items = items == null ? List.of() : List.copyOf(items); + } + public record OrderItemRequest( Long productId, 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/OrderRequest.java` around lines 5 - 11, OrderRequest currently stores the incoming List<OrderItemRequest> reference directly (items), allowing external mutation and nulls; add a compact canonical constructor for OrderRequest that performs defensive copying and null checks: require items non-null (or replace null with empty list if you prefer), validate no null elements (Objects.requireNonNull on each OrderItemRequest), and assign items = List.copyOf(items) (or Collections.unmodifiableList(new ArrayList<>(items))) so the record holds an immutable copy; reference the OrderRequest record and its nested OrderItemRequest type when making these changes.apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java (1)
12-12:findById메서드 선언이 중복된다.
JpaRepository<Order, Long>는 이미Optional<Order> findById(Long id)를 제공한다. 명시적 재선언은 불필요하며, 코드 유지보수 시 혼란을 야기할 수 있다. 제거를 권장한다.수정안
public interface OrderJpaRepository extends JpaRepository<Order, Long> { - Optional<Order> findById(Long id); - Page<Order> 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/infrastructure/order/OrderJpaRepository.java` at line 12, Remove the redundant explicit declaration of Optional<Order> findById(Long id) from the OrderJpaRepository interface; JpaRepository<Order, Long> already provides findById, so delete the findById method declaration in OrderJpaRepository to avoid duplication and potential maintenance confusion.apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java (3)
115-132: cancelOrder의 예외 케이스 테스트가 누락되어 있다.PENDING 상태가 아닌 주문(예: 이미 취소된 주문, 승인된 주문)을 취소할 때 예외가 발생하는지 서비스 레벨에서 검증이 필요하다.
OrderTest에서 도메인 로직 테스트가 있더라도, 서비스 레이어에서 예외가 올바르게 전파되는지 확인해야 운영 시 예기치 않은 동작을 방지할 수 있다.추가 테스트 예시
`@DisplayName`("이미 취소된 주문을 취소하면, BAD_REQUEST 예외가 발생한다.") `@Test` void throwsBadRequest_whenOrderIsAlreadyCancelled() { // Arrange Order order = new Order(1L, "ORD-20240101-ABCD1234", 50000L); order.cancel(); // 먼저 취소 // Act & Assert CoreException exception = assertThrows(CoreException.class, () -> orderService.cancelOrder(order)); assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }🤖 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/OrderServiceTest.java` around lines 115 - 132, Add a service-layer negative test in OrderServiceTest to assert cancelOrder throws the proper exception when order status is not PENDING: create an Order, set its status to a non-PENDING state (e.g., call order.cancel() or set status to CANCELLED/APPROVED), then call orderService.cancelOrder(order) and assert a CoreException is thrown and its getErrorType() equals ErrorType.BAD_REQUEST; reference the existing cancelOrder method and Order class to locate the test and mirror the positive test structure for arrangement, action, and assertion.
40-58: createOrder의 유효성 검증 실패 테스트가 누락되어 있다.
OrderTest에서totalAmount <= 0일 때BAD_REQUEST예외가 발생하는 것을 테스트하고 있다. 서비스 레이어에서도 이 예외가 정상적으로 전파되는지 확인하는 테스트가 필요하다.추가 테스트 예시
`@DisplayName`("totalAmount가 0 이하이면, BAD_REQUEST 예외가 발생한다.") `@Test` void throwsBadRequest_whenTotalAmountIsNotPositive() { // Act & Assert CoreException exception = assertThrows(CoreException.class, () -> orderService.createOrder(1L, "ORD-20240101-ABCD1234", 0L)); assertThat(exception.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST); }🤖 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/OrderServiceTest.java` around lines 40 - 58, Add a negative test in OrderServiceTest to verify service-level validation: in the CreateOrder nested class add a test (e.g., throwsBadRequest_whenTotalAmountIsNotPositive) that calls orderService.createOrder(1L, "ORD-20240101-ABCD1234", 0L) and asserts a CoreException is thrown and its getErrorType() equals ErrorType.BAD_REQUEST; use assertThrows to capture the exception and assert the error type to ensure validation in createOrder is propagated from the service layer.
134-152: approveOrder의 NOT_FOUND 및 상태 예외 테스트가 누락되어 있다.다음 시나리오에 대한 테스트가 필요하다:
- 존재하지 않는 주문 ID로 승인 시
NOT_FOUND예외 발생- 이미 승인된 주문을 다시 승인 시
BAD_REQUEST예외 발생- 취소된 주문을 승인 시
BAD_REQUEST예외 발생운영 환경에서 잘못된 상태 전이 요청이 발생할 경우, 서비스 레이어에서 예외가 올바르게 처리되는지 검증해야 한다.
🤖 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/OrderServiceTest.java` around lines 134 - 152, Add three negative tests inside the ApproveOrder nested test class for orderService.approveOrder: (1) when orderRepository.findById(...) returns Optional.empty() assert that approveOrder throws the service's NOT_FOUND exception; (2) when findById returns an Order with status OrderStatus.CONFIRMED assert that approveOrder throws the service's BAD_REQUEST exception; (3) when findById returns an Order with status OrderStatus.CANCELLED assert that approveOrder throws the service's BAD_REQUEST exception. Use given(orderRepository.findById(…)).willReturn(Optional.empty()) or Optional.of(orderWithStatus) to set up each scenario and use assertThrows against the specific exception type your service raises for NOT_FOUND and BAD_REQUEST.apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java (1)
24-37: signUp과 getMyInfo의 위임 패턴이 일관되지 않다.
getMyInfo는UserFacade를 통해 호출하지만,signUp은UserService를 직접 호출하고 Controller에서UserInfo.from(user)변환을 수행한다. 이 불일치는 다음 문제를 야기할 수 있다:
- 향후 signUp에 크로스 도메인 로직(예: 가입 시 포인트 지급)이 추가될 때 Controller 수정 필요
- 변환 로직 위치가 레이어별로 혼재되어 유지보수 복잡도 증가
UserFacade.signUp을 추가하여 패턴을 통일하거나, 현재 구조를 유지할 경우 주석으로 의도를 명시하는 것을 권장한다.🤖 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/user/UserV1Controller.java` around lines 24 - 37, The signUp controller currently calls UserService.signUp and performs UserInfo.from(user) itself, breaking the delegation pattern used by getMyInfo via UserFacade; add a UserFacade.signUp(loginId, password, name, birthday, email) that handles service calls and returns a UserInfo (or UserV1Dto.UserResponse) and then update UserV1Controller.signUp to delegate to UserFacade.signUp and return the facade result (or convert the facade's UserInfo to UserResponse), keeping transformation logic out of the controller and matching getMyInfo's pattern.apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java (1)
5-9: null 입력에 대한 방어 로직 고려가 필요하다.
from(Brand brand)메서드에서brand가 null일 경우 NPE가 발생한다. 호출부에서 null이 전달되지 않음이 보장된다면 현재 구현도 허용 가능하나, 방어적 프로그래밍 관점에서 명시적 검증을 추가하면 디버깅이 용이해진다.제안 (선택적)
public static BrandInfo from(Brand brand) { + if (brand == null) { + throw new IllegalArgumentException("Brand must not be null"); + } return new BrandInfo(brand.getId(), brand.getName(), brand.getDescription()); }🤖 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/brand/BrandInfo.java` around lines 5 - 9, The static factory BrandInfo.from(Brand brand) lacks a null check and will NPE if called with null; add an explicit defensive validation at the start of BrandInfo.from (e.g., use Objects.requireNonNull(brand, "brand must not be null") or throw an IllegalArgumentException) before accessing brand.getId()/getName()/getDescription(), so the method fails fast with a clear message when a null Brand is passed.apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java (1)
44-96: 잔액 연산의 경계값 테스트가 누락되어 있다.현재 테스트는 정상 케이스와 잔액 부족 예외만 검증한다. 운영 환경에서 예기치 않은 입력으로 인한 장애를 방지하려면 다음 경계값 테스트가 필요하다:
deductBalance(0L): 0 차감 시 동작deductBalance(-1L): 음수 차감 시 예외 발생 여부restoreBalance(0L): 0 복구 시 동작restoreBalance(-1L): 음수 복구 시 예외 발생 여부- 오버플로우:
Long.MAX_VALUE잔액에서 추가 복구 시 동작도메인 레이어에서 입력값 검증이 누락되면 상위 레이어에서 잘못된 데이터가 유입될 수 있다.
🤖 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/user/UserTest.java` around lines 44 - 96, Add boundary tests for User.deductBalance and User.restoreBalance: assert that deductBalance(0L) leaves getBalance unchanged and restoreBalance(0L) leaves getBalance unchanged; assert that negative inputs (deductBalance(-1L) and restoreBalance(-1L)) throw CoreException with ErrorType.BAD_REQUEST; and add a test that calling restoreBalance on a User with balance Long.MAX_VALUE (or restoring an amount that would overflow) either throws CoreException (ErrorType.BAD_REQUEST or overflow-specific) or is safely handled—implement the assertion that matches the domain's intended behavior. Locate tests in the existing nested classes (DeductBalance, RestoreBalance) and use the User, deductBalance, restoreBalance, getBalance, CoreException, and ErrorType symbols to add these cases.apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java (1)
16-24:items를 방어적 복사 없이 보관하면 응답 스냅샷이 외부에서 변조될 수 있다.운영 환경에서
from호출 후 원본 리스트가 수정되면 API 응답과 로그/캐시 데이터가 비결정적으로 바뀌어 장애 추적이 어려워진다.from에서List.copyOf(...)로 불변 복사를 적용하고, null은 빈 리스트로 정규화하는 것이 안전하다. 원본 리스트를 사후 변경해도OrderInfo.items()가 변하지 않는지,items().add(...)가 예외를 던지는지 테스트를 추가해야 한다.변경 제안
public record OrderInfo( Long id, Long userId, String orderNumber, OrderStatus status, Long totalAmount, List<OrderItemInfo> items ) { public static OrderInfo from(Order order, List<OrderItemInfo> items) { + List<OrderItemInfo> safeItems = (items == null) ? List.of() : List.copyOf(items); return new OrderInfo( order.getId(), order.getUserId(), order.getOrderNumber(), order.getStatus(), order.getTotalAmount(), - items + safeItems ); } }As per coding guidelines
**/*.java: "null 처리, 방어적 복사, 불변성 ... 점검" 규칙을 적용한 제안이다.🤖 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/OrderInfo.java` around lines 16 - 24, The from factory method in OrderInfo currently stores the provided items list directly which permits external mutation; update OrderInfo.from to normalize null to an empty list and store an immutable defensive copy (e.g., List.copyOf(items)) so OrderInfo.items() cannot be modified afterward, and add a unit test that calls OrderInfo.from with a mutable list (and with null) then verifies OrderInfo.items() content remains stable and that attempts to mutate OrderInfo.items() throw an exception.apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceTest.java (1)
127-138: 삭제 성공 테스트에 영속화 호출 검증이 없어 회귀 탐지력이 약하다.현재 검증은 메모리 객체 상태(
deletedAt)만 확인하므로 서비스에서 저장 호출이 누락되어도 테스트가 통과할 수 있다. 단위 테스트에서는brandRepository.save(brand)호출 여부까지 검증해 soft delete 영속화 경로를 고정하는 것이 안전하다. 추가로 저장 호출이 1회인지까지 검증하는 테스트를 포함해야 한다.변경 제안
import static org.mockito.BDDMockito.given; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; @@ void softDeletesBrand_whenIdExists() { @@ // Assert assertThat(brand.getDeletedAt()).isNotNull(); + verify(brandRepository).save(brand); }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/BrandServiceTest.java` around lines 127 - 138, Update the test softDeletesBrand_whenIdExists to also verify that the repository persistence path is invoked: after calling brandService.deleteBrand(1L) assert the brand.getDeletedAt() is not null and add a Mockito verify(brandRepository).save(brand) (or verify(brandRepository, times(1)).save(brand)) to ensure brandRepository.save(...) is called exactly once; reference the existing mock brandRepository and the brandService.deleteBrand(...) invocation when adding this assertion.apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java (1)
13-13: 좋아요 ID 목록 조회에서 엔티티 전체 로딩은 대량 사용자 시 불필요한 I/O를 만든다.운영 영향: Line 13 경로는
Like엔티티를 전부 적재하므로, 좋아요 수가 큰 사용자에서 메모리/영속성 컨텍스트 부하가 커져 응답 지연 위험이 증가한다.
수정안:productId만 필요할 때는 ID projection 쿼리를 별도 메서드로 제공하는 것이 낫다.
추가 테스트: DataJpaTest로findProductIdsByUserId가 동일한 결과를 반환하고, 빈 결과를 정상 반환하는 케이스를 추가하는 것이 좋다.수정 예시 diff
import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; @@ List<Like> findByUserId(Long userId); + + `@Query`("select l.productId from Like l where l.userId = :userId") + List<Long> findProductIdsByUserId(`@Param`("userId") Long userId); }As per coding guidelines
**/*Repository*.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/infrastructure/like/LikeJpaRepository.java` at line 13, The current repository method findByUserId(Long userId) loads full Like entities causing heavy I/O for users with many likes; replace/add a projection method that returns only product IDs (e.g., add List<Long> findProductIdsByUserId(Long userId) in LikeJpaRepository) and update callers to use the new method when only productId is needed; also add a DataJpaTest that asserts findProductIdsByUserId returns the same productId set as the original findByUserId mapping and correctly returns an empty list for users with no likes.apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java (1)
34-36: 빈 ID 컬렉션에 대한 가드가 없어 불필요한 DB 호출이 발생한다.운영 영향: 호출량이 높을 때 빈 목록 조회도 매번 SQL을 태우면 DB 부하가 누적되고, 일부 환경에서는
IN ()처리 방식 차이로 예외 리스크가 생길 수 있다.
수정안: Line 34 진입 시ids가 null/empty이면 즉시List.of()를 반환해 조기 종료하는 것이 좋다.
추가 테스트: 빈ids입력 시 빈 리스트 반환 및 JPA repository 호출이 발생하지 않음을 검증하는 테스트를 추가하는 것이 좋다.수정 예시 diff
`@Override` public List<Brand> findAllByIds(Collection<Long> ids) { + if (ids == null || ids.isEmpty()) { + return List.of(); + } return brandJpaRepository.findAllByIdInAndDeletedAtIsNull(ids); }As per coding guidelines
**/*Repository*.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/infrastructure/brand/BrandRepositoryImpl.java` around lines 34 - 36, The method findAllByIds currently always calls brandJpaRepository.findAllByIdInAndDeletedAtIsNull which will execute a DB query for empty or null id collections; add a guard at the start of findAllByIds to return List.of() immediately when ids is null or ids.isEmpty() to avoid the unnecessary DB call, and update/add a unit test that calls findAllByIds with null and empty collections asserting an empty list is returned and that brandJpaRepository.findAllByIdInAndDeletedAtIsNull is not invoked.apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java (1)
120-129: 삭제 성공 테스트가 부수효과를 검증하지 않아 회귀를 놓칠 수 있다.운영 영향: 서비스 코드가 변경되어 실제
delete호출이 빠져도 현재 테스트는 통과하므로, 운영 데이터에 좋아요 잔존이 발생해도 사전에 탐지하지 못할 수 있다.
수정안: Line 128 이후likeRepository.delete(like)호출 여부를verify로 명시 검증하는 편이 안전하다.
추가 테스트: NOT_FOUND 케이스에서delete가 호출되지 않음을never()로 함께 검증하면 예외 경로 회귀도 막을 수 있다.수정 예시 diff
// Act & Assert (no exception thrown) likeService.removeLike(1L, 2L); + verify(likeRepository).delete(like);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/LikeServiceTest.java` around lines 120 - 129, The test deletesLike_whenLikeExists in LikeServiceTest only asserts no exception; update it to explicitly verify that likeRepository.delete(like) is invoked by calling a Mockito verify after likeService.removeLike(1L, 2L) (use the same Like instance returned by given(likeRepository.findByUserIdAndProductId(...))). Also update the NOT_FOUND/exception path test to verify that likeRepository.delete(...) is never() called to prevent regression of skipping deletions; reference the methods likeService.removeLike and likeRepository.delete/ findByUserIdAndProductId in your assertions.apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java (1)
120-134: 목록 조회 테스트가 개수만 확인해 데이터 정합성 회귀를 탐지하기 어렵다.운영 영향: 크기만 맞고 내용이 틀린 응답(잘못된 상품 매핑, 순서 뒤바뀜)이 발생해도 테스트가 통과할 수 있어 API 품질 저하를 사전에 차단하지 못한다.
수정안: 이름/주요 필드 기준으로containsExactly검증을 추가해 결과 정합성을 명시하는 것이 좋다.
추가 테스트: ID 일부 미존재/중복 입력 시 반환 목록이 기대 정책(중복 허용 여부, 순서 보장 여부)을 따르는지 케이스를 분리해 검증하는 것이 좋다.수정 예시 diff
- assertThat(result).hasSize(2); + assertThat(result) + .extracting(Product::getName) + .containsExactly("나이키 신발", "아디다스 티셔츠");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/product/ProductServiceTest.java` around lines 120 - 134, The test returnsProducts_whenIdsExist only asserts size; update it to verify result content and order by asserting key fields (e.g., product names or IDs) using AssertJ's containsExactly on the list returned by productService.getProductsByIds(ids), ensuring the expected Product instances (or DTOs) match those from the given(productRepository.findAllByIds(ids)) stub; also add separate tests for partial-missing IDs and duplicate-ID inputs to validate behavior (duplicate allowance and ordering) per service policy.apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java (1)
57-61: 브랜드 삭제 연쇄 처리에서 엔티티별 save 반복은 대량 데이터에서 병목이 된다현재 구현은 상품 수만큼 update를 반복해 트랜잭션 시간이 길어지고 운영 중 락 경합이 커질 수 있다. 저장소에 브랜드 기준 일괄 soft delete 메서드를 추가해 단건 배치 처리로 전환하는 편이 안전하다. 추가로 서비스 단위 테스트에서 저장소 호출이 1회로 축약되는지 검증하고, 통합 테스트에서 대량 상품 삭제 시 응답 시간과 타임아웃 리스크를 확인해야 한다.
리팩터링 방향 예시
`@Transactional` public void deleteProductsByBrandId(Long brandId) { - productRepository.findAllByBrandId(brandId).forEach(product -> { - product.delete(); - productRepository.save(product); - }); + productRepository.softDeleteByBrandId(brandId); }🤖 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/ProductService.java` around lines 57 - 61, 현재 deleteProductsByBrandId 구현은 productRepository.findAllByBrandId(...).forEach(product -> { product.delete(); productRepository.save(product); })처럼 개별 엔티티를 반복 저장해 대량 처리에서 병목을 유발하니, ProductRepository에 브랜드 기준 일괄 soft delete 메서드(예: softDeleteByBrandId or markDeletedByBrandId(Long brandId))를 추가하고 deleteProductsByBrandId는 해당 일괄 메서드만 호출하도록 교체하세요; 기존 개별 호출(product.delete(), productRepository.save(product))을 제거하고 트랜잭션/쿼리로 한 번에 업데이트되도록 구현한 뒤, 서비스 단위 테스트에서 productRepository의 일괄 호출이 1회만 발생하는지(모의 객체로 verify) 검증하고, 통합 테스트로 대량 상품 삭제 시 응답 시간과 타임아웃 리스크를 확인하세요.apps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerStandaloneTest.java (1)
98-156: 인증 헤더 누락 시나리오에 대한 테스트가 없다.
X-Loopers-LoginId또는X-Loopers-LoginPw헤더가 누락된 경우의 동작을 검증하는 테스트가 없다. 운영 환경에서 클라이언트 오류로 인해 헤더가 누락될 수 있으며, 이때 적절한 에러 응답(400 또는 401)이 반환되는지 확인해야 한다.추가 테스트 제안:
`@DisplayName`("인증 헤더가 누락되면, 400을 반환한다.") `@Test` void returns400_whenAuthHeadersMissing() throws Exception { mockMvc.perform(get("/api/v1/users/me")) .andExpect(status().isBadRequest()); }🤖 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/interfaces/api/user/UserV1ControllerStandaloneTest.java` around lines 98 - 156, Add a test for the missing auth header scenario in the UserV1ControllerStandaloneTest::GetMyInfo nested class: create a new test method (e.g., returns400_whenAuthHeadersMissing) annotated with `@DisplayName` and `@Test` that calls mockMvc.perform(get("/api/v1/users/me")) without setting X-Loopers-LoginId or X-Loopers-LoginPw and asserts .andExpect(status().isBadRequest()); no stubbing of userFacade is required for this case.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java (1)
60-64:generateOrderNumber의 테스트 가능성과 충돌 위험에 대한 개선이 필요하다.
테스트 가능성:
LocalDate.now()를 직접 호출하여 날짜 기반 테스트가 어렵다.Clock을 주입받아 시간을 제어할 수 있도록 개선하면 테스트가 용이해진다.UUID 충돌 위험: 8자리 UUID 부분 문자열(
substring(0, 8))은 약 40억 가지 조합(16^8)을 제공하지만, 동일 날짜에 대량 주문 발생 시 Birthday Paradox로 인해 예상보다 빠르게 충돌이 발생할 수 있다. 일 기준 약 65,000건 이상에서 50% 확률로 충돌이 발생한다.수정안:
- UUID 길이를 12자리 이상으로 확장하거나, 타임스탬프와 시퀀스를 조합하는 방식을 고려한다.
- 또는 DB 유니크 제약과 재시도 로직을 추가한다.
Clock 주입을 통한 테스트 가능성 개선
+import java.time.Clock; + `@RequiredArgsConstructor` `@Component` public class OrderService { private final OrderRepository orderRepository; private final OrderItemRepository orderItemRepository; + private final Clock clock; - public String generateOrderNumber() { - String date = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); + public String generateOrderNumber() { + String date = java.time.LocalDate.now(clock).format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd")); String uuid = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); return "ORD-" + date + "-" + uuid; }🤖 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 60 - 64, The generateOrderNumber method is hard to test and has collision risk; change it to accept/inject a java.time.Clock (use Clock system default in production) so you can control LocalDate.now(clock) in tests, and strengthen the unique portion by increasing the UUID substring to at least 12 characters (or combine a high-resolution timestamp + per-request sequence) to reduce birthday-paradox collisions; additionally ensure persistence enforces a unique constraint on order number and add a retry-on-duplicate save loop (or throw a clear exception) when save fails due to uniqueness to guarantee correctness.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (5)
Round3.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**
📒 Files selected for processing (63)
apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.javaapps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.javaapps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.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/application/order/OrderItemInfo.javaapps/commerce-api/src/main/java/com/loopers/application/order/OrderRequest.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.javaapps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.javaapps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.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/OrderItemRepository.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/order/OrderStatus.javaapps/commerce-api/src/main/java/com/loopers/domain/product/Product.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.javaapps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.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/domain/user/UserService.javaapps/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/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/OrderItemRepositoryImpl.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/brand/BrandV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.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/main/java/com/loopers/interfaces/api/product/ProductV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Dto.javaapps/commerce-api/src/test/java/com/loopers/application/user/UserFacadeTest.javaapps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceTest.javaapps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.javaapps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.javaapps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.javaapps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.javaapps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.javaapps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.javaapps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceTest.javaapps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.javaapps/commerce-api/src/test/java/com/loopers/interfaces/api/user/UserV1ControllerStandaloneTest.java
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
Outdated
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java
Show resolved
Hide resolved
| import java.util.Optional; | ||
|
|
||
| public interface BrandJpaRepository extends JpaRepository<Brand, Long> { | ||
| Optional<Brand> findByNameAndDeletedAtIsNull(String name); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# BrandJpaRepository 파일 위치 확인 및 내용 조회
find . -name "BrandJpaRepository.java" -type f 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 175
🏁 Script executed:
# Brand 엔티티 파일 검색
find . -name "Brand.java" -type f 2>/dev/null | grep -E "entity|domain|model"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 154
🏁 Script executed:
# 마이그레이션 파일 검색 (brand 관련)
find . -name "*.sql" -o -name "*.xml" | xargs grep -l -i "brand" 2>/dev/null | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Repository 관련 파일 구조 확인
git ls-files | grep -i "brand" | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 960
🏁 Script executed:
# Brand 엔티티 파일 내용 조회
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: 1160
🏁 Script executed:
# BrandJpaRepository 파일 내용 조회
cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 724
🏁 Script executed:
# 마이그레이션 디렉토리 구조 확인
find . -type d -name migration -o -type d -name migrations -o -type d -name db 2>/dev/null | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# SQL 파일 전체 검색
find . -name "*.sql" -type f 2>/dev/null | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# BaseEntity 파일 검색 및 내용 조회
find . -name "BaseEntity.java" -type f 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 147
🏁 Script executed:
# BrandService 내용 조회
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: 1942
🏁 Script executed:
# BrandRepositoryImpl 내용 조회
cat -n ./apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1490
🏁 Script executed:
# BaseEntity 내용 조회
cat -n ./modules/jpa/src/main/java/com/loopers/domain/BaseEntity.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2291
🏁 Script executed:
# BrandFacade 내용 조회하여 비즈니스 로직 확인
cat -n ./apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1571
🏁 Script executed:
# BrandRepository 인터페이스 확인
cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 519
🏁 Script executed:
# 데이터베이스 스키마/마이그레이션 파일 검색 (다양한 위치 확인)
find . -type f \( -name "*.sql" -o -name "*.xml" \) 2>/dev/null | grep -i -E "migration|flyway|liquibase|schema" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 프로젝트 구조 확인 (리소스 디렉토리 포함)
find ./apps/commerce-api -type d -name resources 2>/dev/null | head -5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 123
🏁 Script executed:
# resources 하위 db 관련 파일 검색
find ./apps/commerce-api/src -type f -name "*.sql" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 스프링 설정 파일에서 JPA/Hibernate DDL 설정 확인
find ./apps/commerce-api/src -name "application*.properties" -o -name "application*.yml" | xargs cat -n 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1450
🏁 Script executed:
# 전체 resources 디렉토리 구조 확인
find ./apps/commerce-api/src/main/resources -type f 2>/dev/null | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 139
🏁 Script executed:
# 혹시 modules/jpa 쪽에 마이그레이션이 있는지 확인
find ./modules -type f \( -name "*.sql" -o -name "*.xml" \) 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# jpa.yml 파일 확인 (JPA/Hibernate 설정)
find ./apps/commerce-api/src -name "jpa.yml" | xargs cat -n 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# Flyway/마이그레이션 관련 설정 확인
grep -r "flyway\|migration\|hibernate.ddl\|jpa.hibernate.ddl" ./apps/commerce-api/src --include="*.yml" --include="*.properties" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
# 테스트 코드에서 데이터베이스 생성 방식 확인
cat -n ./apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1827
현재 unique 제약이 소프트 삭제 패턴과 충돌하여 브랜드 이름 재사용이 불가능하다.
@Column(name = "name", nullable = false, unique = true)는 데이터베이스 UNIQUE 제약을 name 컬럼 단독에 생성한다. 소프트 삭제 시 deleted_at이 설정되어도 행은 DB에 남아있고, UNIQUE 제약은 삭제된 행도 카운트하므로 동일 이름으로 재등록할 수 없다. findByNameAndDeletedAtIsNull() 애플리케이션 레벨 검사는 통과하지만 데이터베이스 제약 위반으로 save() 시점에 실패한다.
조치 방안:
@Column의unique = true제거(name, deleted_at)복합 UNIQUE 제약 또는WHERE deleted_at IS NULL부분 인덱스 추가(name, deleted_at)복합 인덱스 추가로 조회 성능 확보
대량 데이터 환경에서 인덱스 없이 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/infrastructure/brand/BrandJpaRepository.java`
at line 11, The Brand entity's name column is declared with unique=true which
conflicts with soft-delete and causes save() to fail despite
findByNameAndDeletedAtIsNull passing; remove unique=true from the `@Column` on the
name field in the Brand entity, and instead add a database-level composite
unique constraint or index on (name, deleted_at) (or a partial/filtered unique
index WHERE deleted_at IS NULL if your DB supports it) to enforce uniqueness for
non-deleted rows and also add a composite index on (name, deleted_at) to
preserve lookup performance used by
BrandJpaRepository.findByNameAndDeletedAtIsNull.
| public interface UserJpaRepository extends JpaRepository<User, Long> { | ||
| Optional<User> findByLoginId(String loginId); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.java" | grep -i user | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1145
🏁 Script executed:
find . -type f -name "*Repository*.java" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1848
🏁 Script executed:
# User 엔티티 찾기
fd -e java User.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 150
🏁 Script executed:
# BrandJpaRepository 찾기
fd -e java BrandJpaRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 173
🏁 Script executed:
# Read User entity
wc -l apps/commerce-api/src/main/java/com/loopers/domain/user/User.java
cat -n apps/commerce-api/src/main/java/com/loopers/domain/user/User.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3135
🏁 Script executed:
# Read BrandJpaRepository to see soft-delete pattern
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 724
🏁 Script executed:
# Read UserJpaRepository
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 447
🏁 Script executed:
# Read UserRepositoryImpl
wc -l apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1132
🏁 Script executed:
# Find and read BaseEntity
fd -e java BaseEntity.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 145
🏁 Script executed:
# Check if User entity inherits deletedAt from BaseEntity
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:
# Check Brand entity to confirm it has deletedAt field
cat -n apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java | head -50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1160
🏁 Script executed:
# Read BaseEntity from modules/jpa
cat -n modules/jpa/src/main/java/com/loopers/domain/BaseEntity.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2291
소프트 삭제 필터링 불일치로 인한 논리적 오류 발생 가능
User 엔티티는 BaseEntity를 확장하여 deletedAt 필드를 상속받으므로 소프트 삭제가 적용된다. 그러나 UserJpaRepository.findByLoginId()는 삭제된 사용자를 필터링하지 않는 반면, BrandJpaRepository는 모든 조회 메서드에서 DeletedAtIsNull 조건을 적용한다. 이 불일치로 인해 삭제된 사용자 계정이 로그인 조회 시에도 반환될 수 있어, 인증/인가 오류 또는 보안 문제가 발생할 수 있다.
findByLoginId() 메서드에 DeletedAtIsNull 필터를 추가하고, findById() 등 다른 조회 메서드도 동일한 필터링을 적용해야 한다.
🤖 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/user/UserJpaRepository.java`
around lines 8 - 9, The repository method findByLoginId in UserJpaRepository
returns users regardless of soft-delete; update UserJpaRepository to only return
non-deleted users by adding the DeletedAtIsNull filter to findByLoginId and any
other retrieval methods (e.g., findById) so they mirror BrandJpaRepository's
behavior; locate the interface UserJpaRepository and modify the method
signatures (or add `@Query` variants) to include the deletedAt IS NULL condition
for all read operations that should ignore soft-deleted records.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java
Show resolved
Hide resolved
apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java
Show resolved
Hide resolved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
제목: 도메인 주도 설계 구현
📌 Summary
- 목표: 설계 문서를 기반으로 Brand/Product/Like/Order/User 5개 도메인의 전 레이어(Domain → Infrastructure → Application → Interfaces)를 TDD로 구현하고, 단위 테스트로 도메인 로직 정합성을 검증한다.
전 도메인 일관 적용. 주문 스냅샷, 브랜드 삭제 연쇄, 좋아요 수 비정규화 구현. 단위 테스트 추가.
💬 리뷰 포인트
적절한지, 책임이 과도하게 집중되지 않는지
선택한 판단이 적절한지
복구(user.restoreBalance())를 처리하는 구조가 도메인 책임 관점에서 적절한지
🧭 Context & Decision
문제: 초기에 Facade에만 @transactional을 두었으나, Service 단독 호출 시 트랜잭션이 보장되지 않는 문제가 있었다.
결정: Service와 Facade 양쪽에 @transactional 적용
🛠 트랜잭션 적용 전략
@Transactional, 읽기:@Transactional(readOnly = true)@Transactional, 읽기:@Transactional(readOnly = true)크로스 도메인 조율 시 전체 롤백을 보장하기 위한 외부 경계 역할.
트랜잭션으로 동작. 단, 트랜잭션 경계가 두 레이어에 걸쳐 있어 명시적 관리가 필요.
🏛 Facade 레이어 의존성 현황
BrandService,ProductServiceProductService,BrandServiceLikeService,ProductService,UserService,BrandServiceOrderService,ProductService,UserService,BrandServiceUserService문제: 순수 Domain 레이어라면 @entity, @table 등 JPA 기술을 모르는 것이 이상적이다.
결정: @entity 어노테이션을 Domain Entity(Brand, Product 등)에 직접 적용
호출이 필요해짐. 또한 Domain Service가 Infrastructure 타입을 import하는 의존 방향 역전 문제 발생.
얻음.
문제: 상품 목록 조회 시 브랜드명을 보강하는 과정에서 N+1 쿼리 발생 위험.
결정: fetch join 대신 IN절 배치 조회 + Facade에서 Map 조립
🏗️ Design Overview
변경 범위
RepositoryImpl/JpaRepository, Controller/Dto
엔티티 직접 수신), BrandFacade 트랜잭션 누락 수정, OrderFacade 주문 소유자 검증 추가
주요 컴포넌트 책임
🎯 컴포넌트별 책임 정의
decrease/increase), 좋아요 수 관리, Soft Delete 처리approve/cancel), 주문 소유권 검증likesCount갱신 조율🔁 Flow Diagram
주문 생성 흐름
sequenceDiagram autonumber participant Client participant Controller as OrderV1Controller participant Facade as OrderFacade participant UserSvc as UserService participant ProductSvc as ProductService participant BrandSvc as BrandService participant OrderSvc as OrderService Client->>Controller: POST /api/v1/orders Controller->>Facade: createOrder(loginId, pw, items) Note over Facade: [Transaction Start] Facade->>UserSvc: authenticate (인증 & 잔액 차감) loop 각 주문 상품별 (items) Facade->>ProductSvc: getProduct (재고 검증) Facade->>ProductSvc: decreaseStock (재고 차감) end Facade->>BrandSvc: getBrandsByIds (브랜드 벌크 조회: N+1 방지) Facade->>OrderSvc: createOrder (주문 기본 정보 저장) Facade->>OrderSvc: createOrderItems (스냅샷: 상품명·브랜드명·단가 저장) Note over Facade: [Transaction Commit] <br/>(예외 발생 시 전체 Rollback) Facade-->>Controller: OrderInfo (Response DTO) Controller-->>Client: 200 OK브랜드 삭제 연쇄 흐름
sequenceDiagram autonumber participant Client participant Controller as BrandV1Controller participant Facade as BrandFacade participant ProductSvc as ProductService participant BrandSvc as BrandService Client->>Controller: DELETE /api/v1/brands/{id} Controller->>Facade: deleteBrand(id) Note over Facade: @Transactional 시작 Facade->>ProductSvc: deleteProductsByBrandId (소속 상품 연쇄 soft delete) Facade->>BrandSvc: deleteBrand (Brand soft delete) Note over Facade: @Transactional 종료 (실패 시 전체 롤백) Facade-->>Controller: void Controller-->>Client: 200 OKPR 요약: Domain-Driven Design 5개 도메인 전 계층 TDD 구현
변경 목적
도메인 주도 설계(DDD) 원칙에 따라 Brand, Product, Like, Order, User 5개 도메인의 Domain → Infrastructure → Application → Interfaces 계층을 TDD 방식으로 전수 구현하고, 단위 테스트로 도메인 로직을 검증합니다.
핵심 변경점
리스크 및 확인 사항
테스트 및 검증
각 도메인 엔티티(Brand, Product, Like, Order, User)의 비즈니스 로직과 Service, Facade의 조율 로직을 단위 테스트로 검증(BrandTest, ProductTest, LikeTest, OrderTest, UserTest 등 추가). 트랜잭션 경계, N+1 방지, soft delete 동작, 스냅샷 정합성이 테스트로 보장되는지 확인 필요.