Round3 ecommerce implementation#127
Conversation
테스트 파일이 대상 클래스와 다른 패키지에 위치해 발생하는 구조적 불일치를 해소. 단순 생성/삭제 대신 git mv를 사용해 git 이력(rename)을 보존하며 이동. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ProductOrder(domain) 신설: Repository 계약용 정렬 타입 - ProductSort(application) 유지: API 파라미터용 타입 - ProductSort.toOrder()로 application → domain 변환 위임 - ProductRepository/Impl: ProductSort 의존 제거, ProductOrder 사용 - ProductV1Controller: ProductSort 유지 (API 파라미터 역할 명확화) - ProductService: sort.toOrder() 호출로 Repository에 전달 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:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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: 4
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
172-199:⚠️ Potential issue | 🟡 Minor테스트 이름과 기대 상태코드를 일치시켜야 한다.
현재 비밀번호 불일치 케이스에서 기대값은 404인데 테스트 메서드명이
returnsBadRequest_whenCurrentPasswordNotMatches로 남아 있어 장애 분석 시 CI 리포트 해석을 혼동시킨다. 메서드명을returnsNotFound_whenCurrentPasswordNotMatches로 변경하는 것이 좋다. 추가로존재하지 않는 사용자와비밀번호 불일치를 파라미터화해 모두 404를 보장하는 테스트를 넣으면 인증 예외 흐름 회귀를 안정적으로 막을 수 있다. 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/interfaces/api/UserV1ApiE2ETest.java` around lines 172 - 199, Rename the test method in UserV1ApiE2ETest from returnsBadRequest_whenCurrentPasswordNotMatches to returnsNotFound_whenCurrentPasswordNotMatches so the method name matches the asserted HttpStatus.NOT_FOUND; additionally consider adding a parameterized test variant (e.g., using `@ParameterizedTest`) or a second test in the same class to cover both "non-existent user" and "current password mismatch" scenarios to assert 404 behavior for both error flows.apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java (1)
21-26:⚠️ Potential issue | 🟠 MajorBean Validation 어노테이션 제거하여 도메인 검증 일원화
프로젝트는 Hexagonal Architecture 원칙에 따라 도메인 계층에서만 검증을 수행하고, DTO에서는 Bean Validation 어노테이션을 사용하지 않는다. 현재
UserV1Dto.CreateRequest에@NotBlank,@NotNull이 있고 이 컨트롤러에서@Valid를 사용 중인데, 이는 다음 두 가지 문제를 야기한다:
검증 이중화: 동일 필드에 대해
@Valid(DTO) → MethodArgumentNotValidException 경로와 User.create()(도메인) → CoreException 경로가 동시 존재한다. 도메인 엔티티의 검증 계약이 불명확하고, 다른 진입점(배치, 메시지 큐 등)에서는 DTO 검증이 적용되지 않아 일관성이 깨진다.아키텍처 원칙 위반: 도메인 모듈의 독립성이 손상되고, 검증 로직이 계층 간 산재된다.
UserV1Dto의@NotBlank,@NotNull어노테이션과 해당 import를 제거하고, 이 컨트롤러 및UserV1Controller의@Valid와jakarta.validation.Validimport를 제거한다. 현재 도메인 계층(User 엔티티, PasswordPolicyValidator)의 검증 로직이 이미 모든 필드를 포괄하므로 충분하다.추가 테스트로 빈
loginId, 잘못된 이메일, 미래 생년월일 payload 요청 시 BAD_REQUEST와 표준 ApiResponse 에러 포맷이 반환되는 E2E 케이스를 추가하여 도메인 검증이 정상 동작함을 확인한다.🤖 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/SignUpV1Controller.java` around lines 21 - 26, Remove DTO- and controller-level bean validation to centralize validation in the domain: delete the `@NotBlank/`@NotNull annotations and their imports from UserV1Dto.CreateRequest, remove the `@Valid` annotation and its jakarta.validation.Valid import from the SignUpV1Controller.signUp method (and from UserV1Controller if present), and ensure SignUpCommand.from(request) relies on domain validators (User.create()/PasswordPolicyValidator). After making these removals, add E2E tests that POST payloads with empty loginId, invalid email, and future birthDate to verify the domain validation path returns BAD_REQUEST with the standard ApiResponse error format.
🟠 Major comments (29)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java-218-221 (1)
218-221:⚠️ Potential issue | 🟠 Major주문 목록 테스트가 사용자 격리를 충분히 검증하지 못한다
보안/개인정보 관점에서 목록 API가 타 사용자 주문을 함께 반환해도 현재
isNotEmpty()검증만으로는 테스트가 통과할 수 있다. 응답 목록의 모든userId가savedUser.getId()인지 검증하고, 타 사용자 주문을 미리 생성한 뒤 목록에서 제외되는지까지 검증해야 한다. 추가 테스트로otherUser주문 생성 후savedUser목록 조회 시 해당 주문 ID가 포함되지 않음을 명시적으로 확인하는 케이스를 추가하는 편이 좋다.수정안 제안
assertAll( () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(response.getBody().data()).isNotEmpty() + () -> assertThat(response.getBody()).isNotNull(), + () -> assertThat(response.getBody().data()).isNotEmpty(), + () -> assertThat(response.getBody().data()) + .allSatisfy(order -> assertThat(order.userId()).isEqualTo(savedUser.getId())) );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/interfaces/api/OrderV1ApiE2ETest.java` around lines 218 - 221, 현재 assertAll 블록은 응답이 비어있지 않은지만 확인하므로 사용자 격리를 검증하지 못합니다; OrderV1ApiE2ETest의 assertion을 강화해 response.getBody().data()의 각 항목에 대해 userId가 savedUser.getId()와 일치하는지 검증하고, 테스트 준비 단계에서 otherUser로 주문을 생성한 뒤 savedUser로 목록 조회 시 그 otherUser 주문의 ID가 반환 목록에 포함되지 않는지를 명시적으로 assert 하도록 수정하세요 (참조 심볼: assertAll, response.getBody().data(), savedUser.getId(), otherUser, savedOrder 또는 생성된 다른 주문 ID).apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java-3-14 (1)
3-14:⚠️ Potential issue | 🟠 Major도메인 저장소 인터페이스가 Spring
Page/Pageable에 직접 결합되어 있다도메인 계층이 프레임워크 타입에 묶이면 저장소 구현체 교체, 모듈 재사용, 또는 애플리케이션 분해 시 변경 영향이 확산된다. 현재 설계는 도메인 인터페이스 정의 단계에서 이미 Spring Data 타입을 노출하므로, 테스트 환경의 메모리 구현체도 강제로
Page/PageImpl에 의존하게 된다.수정안은 도메인 계층에 프레임워크 비의존 페이징 질의/결과 모델(예:
BrandPageQuery,BrandPageResult)을 두고,Pageable변환은 인프라 구현체에서만 처리하는 것이다. 테스트 환경의 인메모리 구현도 순수 컬렉션 기반 페이징으로 단순화할 수 있다.인프라 어댑터 단위 테스트에서 정렬/페이지번호/총 개수 매핑이 정확히 유지되는지 검증하는 케이스를 추가하는 것이 좋다.
리팩터 방향 예시
-import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - Page<Brand> findAllByDeletedAtIsNull(Pageable pageable); + BrandPageResult findAllActive(BrandPageQuery query);🤖 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/BrandRepository.java` around lines 3 - 14, The BrandRepository interface currently exposes Spring types (Page, Pageable) — change the domain API to be framework-agnostic by replacing findAllByDeletedAtIsNull(Pageable pageable) with a method using domain paging DTOs, e.g., Page/Query types you create such as BrandPageQuery and BrandPageResult; keep signatures in BrandRepository like BrandPageResult findAllByDeletedAtIsNull(BrandPageQuery query) and leave save, findById, findAllByIdIn as-is; then move all Pageable-to-BrandPageQuery and BrandPageResult-to-Page conversions into the infrastructure adapter that implements BrandRepository so only the adapter deals with Spring Pageable/Page, and add adapter unit tests to verify page number, size, sort and total count mapping.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java-33-40 (1)
33-40:⚠️ Potential issue | 🟠 Major주문 항목의 핵심 필드 검증이 누락되어 데이터 무결성 리스크가 있다.
Line 33-40, 46-49에서
quantity만 검증하고orderId,productId,productName,price를 검증하지 않는다. 운영에서는 잘못된 주문 항목이 저장되어 주문 합계 불일치, 정산 오류, 장애 대응 난이도 증가로 이어질 수 있다. 생성자에서 필수 필드를 함께 검증하도록 수정이 필요하다.수정안 예시
private OrderItem(Long orderId, Long productId, String productName, Integer price, Integer quantity) { + validateOrderId(orderId); + validateProductId(productId); + validateProductName(productName); + validatePrice(price); validateQuantity(quantity); this.orderId = orderId; this.productId = productId; this.productName = productName; this.price = price; this.quantity = quantity; } @@ + private void validateOrderId(Long orderId) { + if (orderId == null || orderId < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 ID는 1 이상이어야 합니다."); + } + } + + private void validateProductId(Long productId) { + if (productId == null || productId < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 ID는 1 이상이어야 합니다."); + } + } + + private void validateProductName(String productName) { + if (productName == null || productName.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 비어 있을 수 없습니다."); + } + } + + private void validatePrice(Integer price) { + if (price == null || price < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 가격은 0 이상이어야 합니다."); + } + } + private void validateQuantity(Integer quantity) { if (quantity == null || quantity < 1) { throw new CoreException(ErrorType.BAD_REQUEST, "주문 수량은 1개 이상이어야 합니다."); } }추가 테스트로
orderId/productId null,productName blank,price null/음수에 대한 예외 케이스를 각각 분리해 검증해야 한다.
As per coding guidelines, "**/domain/**/*.java: 엔티티/값 객체/DTO 경계를 명확히 하고, 불변성과 캡슐화를 점검한다."Also applies to: 46-49
🤖 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 33 - 40, The private OrderItem(Long orderId, Long productId, String productName, Integer price, Integer quantity) constructor currently only calls validateQuantity(quantity); add validation for orderId and productId being non-null, productName being non-null/non-blank, and price being non-null and > 0 (throw IllegalArgumentException or use Objects.requireNonNull with clear messages) before assigning fields; apply the same validations to the other OrderItem constructor/creation path referenced in this class (the one at lines 46-49) so all creation code paths enforce orderId/productId non-null, productName non-blank, price positive, and quantity validated via validateQuantity.apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemSnapshot.java-3-11 (1)
3-11:⚠️ Potential issue | 🟠 Major스냅샷 생성 시 입력 검증이 없어 음수 금액/수량이 주문 계산으로 유입될 수 있다.
Line 3-11에서
productId,productName,unitPrice,quantity검증이 없어 비정상 입력이 그대로 누적 금액 계산으로 전달된다. 운영에서는 주문 총액 음수, 재고 차감 불일치, 장애 원인 추적 난이도 증가가 발생할 수 있다. record compact constructor에서 즉시 검증하도록 수정이 필요하다.수정안 예시
package com.loopers.application.order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + public record OrderItemSnapshot( Long productId, String productName, int unitPrice, int quantity ) { + public OrderItemSnapshot { + if (productId == null || productId < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 ID는 1 이상이어야 합니다."); + } + if (productName == null || productName.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 비어 있을 수 없습니다."); + } + if (unitPrice < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "단가는 0 이상이어야 합니다."); + } + if (quantity < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "수량은 1 이상이어야 합니다."); + } + } + public long lineAmount() { return (long) unitPrice * quantity; } }추가 테스트로
unitPrice < 0,quantity < 1,productId null,productName blank예외 케이스와 정상 입력에서lineAmount계산 정확성 케이스를 함께 추가해야 한다.
Based on learnings, "In the loop-pack-be-l2-vol3-java project, 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/application/order/OrderItemSnapshot.java` around lines 3 - 11, Add input validation to the OrderItemSnapshot record by implementing a compact constructor in OrderItemSnapshot that checks productId != null, productName is not blank, unitPrice >= 0, and quantity >= 1; when a check fails, throw the project's unified CoreException (so ApiControllerAdvice handles it) with a clear message; keep the existing lineAmount() as-is but rely on validated fields; also add unit tests covering unitPrice < 0, quantity < 1, productId null, productName blank, and a normal case verifying lineAmount() correctness.apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java-28-36 (1)
28-36:⚠️ Potential issue | 🟠 Major도메인 생성 시 필수 불변식 검증이 누락되어 있다.
Line 28-35에서
userId와totalAmount를 검증하지 않아 잘못된 주문이 생성될 수 있다. 운영 시점에는 DB 제약 위반이 커밋 구간에서 500으로 터지거나, 음수 주문 금액이 저장되어 정산/집계 데이터가 오염될 수 있다. 생성 시점에 즉시CoreException으로 차단하는 방식으로 수정이 필요하다.수정안 예시
import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; @@ private Order(Long userId, Long totalAmount) { + validateUserId(userId); + validateTotalAmount(totalAmount); this.userId = userId; this.status = Status.ORDERED; this.totalAmount = totalAmount; } @@ public static Order create(Long userId, Long totalAmount) { return new Order(userId, totalAmount); } + + private void validateUserId(Long userId) { + if (userId == null || userId < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 1 이상이어야 합니다."); + } + } + + private void validateTotalAmount(Long totalAmount) { + if (totalAmount == null || totalAmount < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "총 주문 금액은 0 이상이어야 합니다."); + } + }추가 테스트로
Order.create(null, 1000L),Order.create(1L, null),Order.create(1L, -1L)가 모두BAD_REQUEST를 던지는 케이스를 포함해야 한다.
Based on learnings, "In the loop-pack-be-l2-vol3-java project, 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/order/Order.java` around lines 28 - 36, The Order factory is not validating required invariants: update the private constructor Order(Long userId, Long totalAmount) (or the public static create(Long userId, Long totalAmount)) to validate that userId and totalAmount are non-null and that totalAmount is >= 0, and throw CoreException (with appropriate BAD_REQUEST error code) when any check fails so errors flow through ApiControllerAdvice; add unit tests calling Order.create(null, 1000L), Order.create(1L, null), and Order.create(1L, -1L) expecting CoreException/BAD_REQUEST to ensure the guards work.apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java-36-43 (1)
36-43:⚠️ Potential issue | 🟠 Major도메인 생성 시 입력 검증이 없어 DB 예외로 누수될 수 있다.
Line 41-43에서
userId,productId검증이 없으면 잘못된 입력이 DB 레이어까지 내려가DataIntegrity계열 예외로 표면화되어, 운영에서 에러 포맷이 불일치하거나 500으로 보일 위험이 있다.
수정안으로Like.create내부에서 null/0 이하를 검증하고CoreException(ErrorType.BAD_REQUEST, ...)를 던지도록 도메인 경계에서 차단하는 것이 필요하다.
추가 테스트로Like.create(null, 1L),Like.create(0L, 1L),Like.create(1L, null),Like.create(1L, -1L)에 대한BAD_REQUEST케이스를 도메인 테스트로 보강하는 것이 좋다.Based on learnings: In the loop-pack-be-l2-vol3-java project, 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/like/Like.java` around lines 36 - 43, Add input validation inside the Like.create factory: check that userId and productId are non-null and >0, and if any check fails throw new CoreException(ErrorType.BAD_REQUEST, "invalid like input") (or a clear message) so invalid values are rejected at the domain boundary; update/create domain tests to assert that Like.create(null,1L), Like.create(0L,1L), Like.create(1L,null) and Like.create(1L,-1L) all result in CoreException with ErrorType.BAD_REQUEST.apps/commerce-api/src/main/java/com/loopers/application/like/LikeApplicationService.java-19-26 (1)
19-26:⚠️ Potential issue | 🟠 Major동시 등록 시 경쟁 상태로 의도치 않은 500이 발생할 수 있다.
Line 19-26은
find -> save순서라 동시에 같은(userId, productId)요청이 들어오면 둘 다 조회를 통과하고 저장에서 유니크 제약 예외(DataIntegrityViolationException)가 발생한다. 현재는 이를 처리하지 않아 글로벌 예외 핸들러의 일반Throwable캐치로 빠져ErrorType.INTERNAL_ERROR(500)로 응답되므로, 운영에서ALREADY_LIKED(400) 대신 내부 오류로 노출된다.해결책:
register()메서드에서 저장 시DataIntegrityViolationException을try-catch로 잡아CoreException(ErrorType.ALREADY_LIKED, "이미 좋아요한 상품입니다.")로 변환한다. 선조회는 제거하고 DB 예외 기반으로 단순화한다.추가 테스트: 동시성 시나리오(동일 사용자/상품에 대해 병렬
register)를 추가해 결과가 "성공 1건 + ALREADY_LIKED 1건"으로 고정되고 500이 발생하지 않음을 검증한다.🤖 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/LikeApplicationService.java` around lines 19 - 26, Remove the pre-check in LikeApplicationService.register and instead wrap the save call in a try-catch that catches org.springframework.dao.DataIntegrityViolationException; on catch, throw new CoreException(ErrorType.ALREADY_LIKED, "이미 좋아요한 상품입니다."); keep creating the Like via Like.create(userId, productId) and persist with likeRepository.save(like) inside the try block, and add a concurrency test that runs parallel register(userId, productId) calls asserting one success and the other throws CoreException with ErrorType.ALREADY_LIKED.apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java-292-295 (1)
292-295:⚠️ Potential issue | 🟠 Major정렬 검증이 누락을 놓칠 수 있어 회귀 탐지 신뢰도가 낮다.
운영 관점에서
indexOf기반 비교는 응답에서 특정 상품이 누락되어도-1비교로 통과할 수 있어, 정렬/필터 회귀가 배포까지 누락될 위험이 있다.
수정안으로 정렬 테스트는 인덱스 비교 대신 기대 ID 순서를 직접 검증하도록containsExactly(...)형태로 바꾸는 것이 안전하다.
추가 테스트로 “정렬 대상 중 1개가 응답에서 누락된 경우 반드시 실패” 시나리오를 넣어 오탐을 차단해야 한다.🔧 제안 수정안
- () -> assertThat(ids.indexOf(cheapProduct.getId())) - .isLessThan(ids.indexOf(middleProduct.getId())) - .isLessThan(ids.indexOf(expensiveProduct.getId())) + () -> assertThat(ids) + .containsExactly(cheapProduct.getId(), middleProduct.getId(), expensiveProduct.getId()) ... - () -> assertThat(ids.indexOf(highLikes.getId())) - .isLessThan(ids.indexOf(middleLikes.getId())) - .isLessThan(ids.indexOf(lowLikes.getId())) + () -> assertThat(ids) + .containsExactly(highLikes.getId(), middleLikes.getId(), lowLikes.getId())As per coding guidelines:
**/*Test*.java: 통합 테스트는 격리 수준, 플래키 가능성, 테스트 데이터 준비/정리 전략을 점검한다.Also applies to: 331-334
🤖 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/ProductV1ApiE2ETest.java` around lines 292 - 295, Replace the fragile indexOf-based ordering assertion with a direct equality check of the expected ID sequence: assert that the response IDs list (ids) containsExactly the expected order built from cheapProduct.getId(), middleProduct.getId(), expensiveProduct.getId() so missing items cannot silently pass; update the assertions in ProductV1ApiE2ETest where ids.indexOf(...) is used (the block referencing cheapProduct, middleProduct, expensiveProduct) and add an additional negative test case that simulates one of the expected products missing from the response and asserts the test fails (or that containsExactly does not match) to prevent false positives from omitted items.apps/commerce-api/src/test/java/com/loopers/domain/brand/InMemoryBrandRepository.java-15-16 (1)
15-16:⚠️ Potential issue | 🟠 Major인메모리 Repository가 Pageable 계약을 지키지 않아 페이징 회귀를 숨길 수 있다.
운영 관점에서 테스트 더블이
pageable을 무시하면 실제 환경에서 발생할 페이징 버그를 테스트가 잡지 못하고,HashMap순서 비결정성으로 플래키 테스트가 생길 수 있다.
수정안으로 필터 결과를 안정적인 순서(예: id 오름차순)로 정렬한 뒤offset/pageSize를 적용해PageImpl을 구성해야 한다.
추가 테스트로 page size=2, page=0/1/2 케이스에서content.size,totalElements, 순서가 모두 기대값과 일치하는지 검증해야 한다.🔧 제안 수정안
+import java.util.Comparator; -import java.util.HashMap; +import java.util.LinkedHashMap; ... - private final Map<Long, Brand> store = new HashMap<>(); + private final Map<Long, Brand> store = new LinkedHashMap<>(); ... public Page<Brand> findAllByDeletedAtIsNull(Pageable pageable) { - var list = store.values().stream() - .filter(b -> b.getDeletedAt() == null) - .toList(); - return new PageImpl<>(list, pageable, list.size()); + var filtered = store.values().stream() + .filter(b -> b.getDeletedAt() == null) + .sorted(Comparator.comparingLong(Brand::getId)) + .toList(); + + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), filtered.size()); + var pageContent = start >= filtered.size() ? List.<Brand>of() : filtered.subList(start, end); + return new PageImpl<>(pageContent, pageable, filtered.size()); }As per coding guidelines:
**/*Repository*.java: 쿼리 조건 누락/과다 조회, 정렬/인덱스 활용 가능성, 대량 데이터에서의 병목을 점검한다.Also applies to: 39-43
🤖 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/InMemoryBrandRepository.java` around lines 15 - 16, The in-memory repository (InMemoryBrandRepository) currently ignores Pageable and uses an unordered HashMap (store) which can hide paging bugs and cause flaky tests; update the repository's pageable-aware retrieval (e.g., the method that returns Page<Brand>) to collect matching Brands from store, sort them by id ascending (use the id field and idGenerator semantics), then apply offset = page.getOffset() and limit = page.getSize() to slice the sorted list and build a PageImpl with correct totalElements and content; also add unit tests for pageSize=2 with page 0,1,2 asserting content.size, totalElements, and that returned Brands are in ascending id order to catch regressions.apps/commerce-api/src/main/java/com/loopers/interfaces/api/auth/LoginUserInterceptor.java-24-26 (1)
24-26:⚠️ Potential issue | 🟠 Major인증 누락을 NOT_FOUND로 처리하면 장애·보안 신호가 왜곡된다.
운영 관점에서 인증 실패를 404로 응답하면 모니터링에서 인증 오류와 리소스 부재를 구분하지 못해 탐지/대응 품질이 떨어진다.
수정안으로 인증 헤더 누락은 인증 실패 전용 에러 타입(프로젝트의 401 매핑 타입)으로 변경하고,null뿐 아니라 blank 값도 차단해야 한다.
추가 테스트로 보호 엔드포인트에 대해 “헤더 누락/공백 → 401 계열 + 표준 에러 포맷” 회귀 테스트를 추가해야 한다.🔧 제안 수정안
+import org.springframework.util.StringUtils; ... - if (loginId == null || loginPw == null) { - throw new CoreException(ErrorType.NOT_FOUND, "인증 정보가 없습니다."); + if (!StringUtils.hasText(loginId) || !StringUtils.hasText(loginPw)) { + throw new CoreException(ErrorType.UNAUTHORIZED, "인증 정보가 없습니다."); }🤖 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/auth/LoginUserInterceptor.java` around lines 24 - 26, In LoginUserInterceptor, change the error thrown when loginId or loginPw are missing from ErrorType.NOT_FOUND to the project's authentication-failure error type that maps to 401, and expand the condition to treat null or blank (empty or whitespace-only) values as missing; update the thrown CoreException message to the standard auth-missing message/format. Also add a regression test for the protected endpoint(s) that asserts when Authorization/login headers are omitted or blank the response status is 401 and the body follows the project’s standard error format.apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/dto/BrandV1Dto.java-4-4 (1)
4-4:⚠️ Potential issue | 🟠 MajorDTO의 Bean Validation 제거 및 도메인 레이어 검증으로 이동 필요다.
이 프로젝트는 헥사고날 아키텍처 원칙에 따라 모든 검증을 도메인 레이어에서 수행하도록 설계되었다. DTO 레이어의 Bean Validation은 이 원칙과 어긋난다. 현재
@NotBlank및jakarta.validation의존을 제거하고,Brand.create(...)/Brand.update(...)도메인 메서드에서 검증하도록 이동해야 한다.추가로 프로젝트 전체 DTO(UserV1Dto, ProductV1Dto, OrderV1Dto)에서도 Bean Validation 어노테이션을 제거하여 정합성을 맞춰야 한다. 도메인 검증 후 발생한 예외가 모두 CoreException → ApiControllerAdvice를 통해 통일된 에러 포맷으로 반환되는지 테스트로 확인하다.
🔧 제안 수정안
-import jakarta.validation.constraints.NotBlank; ... public record CreateRequest( - `@NotBlank`(message = "브랜드 이름은 필수값입니다.") String name, String description ) {} public record UpdateRequest( - `@NotBlank`(message = "브랜드 이름은 필수값입니다.") String name, String description ) {}🤖 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/brand/dto/BrandV1Dto.java` at line 4, DTO들에서 Bean Validation 어노테이션과 jakarta.validation 의존을 제거하세요: apps/commerce-api/src/main/java/.../BrandV1Dto.java의 import jakarta.validation.constraints.NotBlank 및 모든 `@NotBlank/`@Valid 같은 어노테이션을 삭제하고 동일하게 UserV1Dto, ProductV1Dto, OrderV1Dto의 DTO들에서도 제거합니다; 대신 도메인 쪽의 Brand.create(...) 및 Brand.update(...) 메서드에 입력 검증 로직을 구현하여 빈값/유효성 체크를 수행하게 하고, 도메인 검증 실패 시 던지는 예외가 CoreException 계층으로 래핑되어 ApiControllerAdvice에서 일관된 에러 포맷으로 반환되는지 확인하는 단위/통합 테스트를 추가하세요.apps/commerce-api/src/main/java/com/loopers/application/user/SignUpApplicationService.java-18-20 (1)
18-20:⚠️ Potential issue | 🟠 Major동시 가입 요청에서 중복 로그인 ID 방지가 불완전하다.
findByLoginId사전조회만으로는 TOCTOU 경쟁 조건이 발생하여 동일loginId를 가진 두 개의 User 레코드가 데이터베이스에 삽입되거나,DataIntegrityViolationException이 전역 핸들러에서 INTERNAL_ERROR(500)로 노출될 수 있다. User 엔티티의@Column(unique = true)제약을 전제로save호출 시 DB 무결성 예외를CoreException(CONFLICT)로 변환하는 처리가 필수다.🔧 제안 수정안
+import org.springframework.dao.DataIntegrityViolationException; + `@Service` `@Transactional` `@RequiredArgsConstructor` public class SignUpApplicationService { private final PasswordEncoder passwordEncoder; private final UserRepository userRepository; public void signUp(SignUpCommand command) { if (userRepository.findByLoginId(command.loginId()).isPresent()) { throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다."); } PasswordPolicyValidator.validate(command.password(), command.birthDate()); String encodedPassword = passwordEncoder.encode(command.password()); User user = User.create(command, encodedPassword); - userRepository.save(user); + try { + userRepository.save(user); + } catch (DataIntegrityViolationException e) { + throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다."); + } } }추가로 동일
loginId로 2개의 동시 가입 요청을 실행하여 1건은 성공하고 1건은 409 CONFLICT를 반환하는지 동시성 테스트로 검증해야 한다.🤖 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/user/SignUpApplicationService.java` around lines 18 - 20, The pre-check in SignUpApplicationService using userRepository.findByLoginId(...) is vulnerable to TOCTOU race conditions; update the sign-up flow to catch database integrity errors on userRepository.save(...) and translate DataIntegrityViolationException (or the specific JPA/DB exception) into throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다."); keep the existing pre-check (findByLoginId) for fast-fail but ensure save is wrapped in a try/catch that maps integrity violations to CoreException(CONFLICT) so duplicate unique constraint violations are returned as 409; add a concurrency test that issues two simultaneous sign-ups with the same loginId and asserts one succeeds and the other receives a 409 CONFLICT.apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java-72-74 (1)
72-74:⚠️ Potential issue | 🟠 Major가시성 변경 메서드가 null을 허용해 런타임 실패를 유발한다
운영 관점에서
visibility=null상태는 DB 저장 시 제약조건 위반 또는 이후 로직의 NPE를 유발해 트랜잭션 롤백과 장애를 만든다. 수정안으로 null 검증 후CoreException을 던지도록 방어해야 한다.수정 예시
public void changeVisibility(Visibility visibility) { + if (visibility == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "노출 상태는 필수값입니다."); + } this.visibility = visibility; }추가 테스트로
changeVisibility(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/domain/product/Product.java` around lines 72 - 74, The changeVisibility method in Product currently allows a null value which can cause DB constraint failures or NPEs; update Product.changeVisibility(Visibility visibility) to validate visibility != null and throw a CoreException with a clear message (include identifying context such as product id or name if available) when null is passed, and add a unit test that calls changeVisibility(null) and asserts the CoreException is thrown; ensure you update any callers or documentation if they relied on null semantics.apps/commerce-api/src/main/java/com/loopers/application/order/OrderCreateCommand.java-8-11 (1)
8-11:⚠️ Potential issue | 🟠 Major요청 값 null/empty에서 NPE가 발생해 500으로 전이될 수 있다
운영 관점에서 잘못된 사용자 입력이 4xx가 아닌 500으로 기록되면 장애 알람 노이즈와 원인 추적 비용이 증가한다. 수정안으로
request,request.items()null/empty와 item 필드 유효성을 명시 검증하고CoreException(BAD_REQUEST)로 변환해야 한다.수정 예시
public record OrderCreateCommand(Long userId, List<OrderItemCommand> items) { public static OrderCreateCommand from(Long userId, OrderV1Dto.CreateRequest request) { + if (request == null || request.items() == null || request.items().isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 상품은 1개 이상이어야 합니다."); + } List<OrderItemCommand> items = request.items().stream() + .peek(item -> { + if (item.productId() == null || item.quantity() == null || item.quantity() <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 상품 정보가 올바르지 않습니다."); + } + }) .map(item -> new OrderItemCommand(item.productId(), item.quantity())) .toList(); return new OrderCreateCommand(userId, items); } }추가 테스트로 null/빈 목록/수량 0 입력에 대해
CoreException이 발생하고 API 응답이 일관된 포맷으로 내려오는지 검증해야 한다. Based on learnings: validation errors should be unified through CoreException and ApiControllerAdvice.🤖 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/OrderCreateCommand.java` around lines 8 - 11, In OrderCreateCommand.from(Long, OrderV1Dto.CreateRequest) add explicit input validation: check request != null, request.items() != null and not empty, and validate each item's productId and quantity (>0); on any validation failure throw new CoreException(BAD_REQUEST) (so ApiControllerAdvice will format the response) instead of letting an NPE occur; ensure the mapping to List<OrderItemCommand> only runs after validation and add unit tests asserting CoreException is thrown for null request, null/empty items list, and item with quantity 0.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java-5-7 (1)
5-7:⚠️ Potential issue | 🟠 MajorDTO의 Bean Validation 어노테이션을 제거하고 도메인 계층 검증으로 통일한다
현재 OrderV1Dto의
@NotNull,@NotEmpty,@Min어노테이션은 프로젝트 아키텍처 원칙과 맞지 않다. OrderItem에서 이미validateQuantity()로 domain layer 검증을 수행하고 있으므로, DTO 어노테이션은 불필요 중복이다. DTO 어노테이션을 제거하고, 주문 항목의 null/빈 목록/0 이하 수량에 대한 모든 검증을 domain layer(Order, OrderItem 생성자)로 이행해야 한다. 추가로 null items, items=[], quantity=0 등의 요청에 대해 일관된 CoreException → ApiControllerAdvice 경로로 에러가 반환되는지 E2E 테스트로 검증한다.🤖 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 5 - 7, Remove the Bean Validation annotations (`@NotNull`, `@NotEmpty`, `@Min`) from OrderV1Dto and rely on domain-layer validation instead: ensure Order and OrderItem constructors (and OrderItem.validateQuantity()) perform null/empty items and quantity<=0 checks and throw the project's CoreException on violation; update any factory/mapper that builds Order/OrderItem from OrderV1Dto to let those domain exceptions bubble up; and add E2E tests that submit payloads with null items, empty items list, and quantity=0 to verify the error flows through ApiControllerAdvice into the API response.apps/commerce-api/src/test/java/com/loopers/domain/product/InMemoryProductRepository.java-49-57 (1)
49-57:⚠️ Potential issue | 🟠 Major테스트 저장소의 정렬·페이징 동작이 운영 구현과 불일치한다.
Line 55-57은
Pageable슬라이싱을 적용하지 않아 전체 목록을 반환하고, Line 51-53의LIKES_DESC가id기준으로 대체되어 운영 쿼리와 다른 결과를 만든다. 이 상태는 테스트 신뢰도를 떨어뜨려 운영 장애를 사전에 놓칠 위험이 크다. 운영 저장소와 동일하게likeCount정렬과 페이지 슬라이싱을 적용해야 한다.수정안 예시
Comparator<Product> comparator = switch (sort) { case PRICE_ASC -> Comparator.comparing(Product::getPrice); - // likes_desc는 Like 도메인 구현 전이므로 인메모리에서 id 기준으로 대체 - case LIKES_DESC, LATEST -> Comparator.comparing(Product::getId).reversed(); + case LIKES_DESC -> Comparator.comparing(Product::getLikeCount).reversed() + .thenComparing(Comparator.comparing(Product::getId).reversed()); + case LATEST -> Comparator.comparing(Product::getId).reversed(); }; List<Product> list = stream.sorted(comparator).toList(); - return new PageImpl<>(list, pageable, list.size()); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), list.size()); + List<Product> content = start >= list.size() ? List.of() : list.subList(start, end); + return new PageImpl<>(content, pageable, list.size());추가 테스트로는
findActiveProducts에 대해 (1)LIKES_DESC정렬 정확성, (2)page/size반영 여부를 각각 검증하는 단위 테스트를 추가해야 한다.🤖 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/InMemoryProductRepository.java` around lines 49 - 57, In InMemoryProductRepository, the current sort/comparator and paging are incorrect: change the LIKES_DESC branch in the comparator switch to sort by Product::getLikeCount (descending) instead of getId, and after obtaining the sorted List via stream.sorted(comparator).toList(), apply Pageable slicing using pageable.getOffset() and pageable.getPageSize() (compute start/end bounds safely against list.size()) to create the PageImpl from the sublist while passing the full list.size() as total; update references to comparator, sort, stream.sorted, and the PageImpl(...) call accordingly and add unit tests that assert LIKES_DESC ordering by likeCount and that page/size are honored.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/dto/ProductV1Dto.java-5-7 (1)
5-7:⚠️ Potential issue | 🟠 MajorDTO에서 Bean Validation 어노테이션을 제거하고 도메인 검증으로 단일 예외 흐름을 유지한다.
현재 ProductV1Dto의 CreateRequest, UpdateRequest에서
@NotNull,@NotBlank를사용하면@Valid에의해 MethodArgumentNotValidException이 발생하여 ApiControllerAdvice의 라인 51 핸들러로 처리된다. 운영 중 요청 검증 실패와 도메인 검증 실패가 다른 예외 경로로 분기되므로 에러 처리 복잡도가 높아진다.Product 도메인의 validateBrandId(), validateName(), validatePrice(), validateStockQuantity() 메서드가 이미 모든 검증을 CoreException으로 처리하고 있으므로, 요청 DTO에서는 어노테이션을 제거하고 모든 검증을 도메인 레이어에서만 수행하도록 수정한다.
수정안
import com.loopers.application.product.ProductInfo; import com.loopers.domain.product.Product; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import java.time.ZonedDateTime; @@ public record CreateRequest( - `@NotNull` Long brandId, - `@NotBlank`(message = "상품 이름은 필수값입니다.") String name, + Long brandId, + String name, String description, - `@NotNull` Integer price, - `@NotNull` Integer stockQuantity + Integer price, + Integer stockQuantity ) {} public record UpdateRequest( - `@NotBlank`(message = "상품 이름은 필수값입니다.") String name, + String name, String description, - `@NotNull` Integer price, - `@NotNull` Integer stockQuantity, + Integer price, + Integer stockQuantity, Product.Visibility visibility ) {}API E2E 테스트에서 brandId=null, name=null/blank, price=null 요청 시 ApiControllerAdvice의 CoreException 핸들러(라인 28)로만 처리되고 BAD_REQUEST 응답이 반환되는지 검증한다.
🤖 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/product/dto/ProductV1Dto.java` around lines 5 - 7, DTO에 Bean Validation 어노테이션(`@NotNull`, `@NotBlank`) 사용으로 인해 요청 검증이 MethodArgumentNotValidException 경로로 분기되므로 ProductV1Dto의 CreateRequest 및 UpdateRequest에서 해당 어노테이션들을 제거하고 `@Valid가` 더 이상 트리거되지 않도록 하여 모든 유효성 검증을 Product 도메인으로 일원화하세요; 구체적으로 ProductV1Dto 내 CreateRequest/UpdateRequest 클래스에서 jakarta.validation.constraints.* 어노테이션을 삭제하고 컨트롤러에서 `@Valid` 검사에 의존하지 않으며, 요청을 도메인으로 전달한 뒤 Product의 validateBrandId(), validateName(), validatePrice(), validateStockQuantity()에서 CoreException을 발생시키도록 유지해 ApiControllerAdvice의 CoreException 핸들러(라인 28)로만 처리되게 만드세요.apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java-47-57 (1)
47-57:⚠️ Potential issue | 🟠 MajorPRICE_ASC와 LIKES_DESC 정렬 시 동일값 존재 시 페이징이 비결정적이 된다.
현재 코드는 단일 정렬 키만 사용하므로
price나likeCount가 동일한 데이터가 많을 때 페이지 간 중복이나 누락이 발생할 수 있다. 2차 정렬 키로id를 추가해 결과를 결정적으로 고정해야 한다.LATEST의 경우 이미id.desc()로 정렬되므로 추가 수정 불필요하다.수정안 예시
- OrderSpecifier<?> orderSpecifier = switch (order) { - case PRICE_ASC -> product.price.asc(); - case LIKES_DESC -> product.likeCount.desc(); + OrderSpecifier<?>[] orderSpecifiers = switch (order) { + case PRICE_ASC -> new OrderSpecifier<?>[]{product.price.asc(), product.id.desc()}; + case LIKES_DESC -> new OrderSpecifier<?>[]{product.likeCount.desc(), product.id.desc()}; case LATEST -> product.id.desc(); }; @@ - .orderBy(orderSpecifier) + .orderBy(orderSpecifiers)
PRICE_ASC/LIKES_DESC에서 동일 값 다건 데이터로 2페이지 이상 조회 시 페이지 간 중복 또는 누락이 없는지 확인하는 통합 테스트를 추가해야 한다.🤖 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 47 - 57, The ordering is currently non-deterministic for PRICE_ASC and LIKES_DESC because only one key is used; update the OrderSpecifier construction in ProductRepositoryImpl so PRICE_ASC becomes product.price.asc().and(product.id.asc()) and LIKES_DESC becomes product.likeCount.desc().and(product.id.desc()) (leave LATEST as product.id.desc()), then run/extend the queryFactory.selectFrom(...).orderBy(orderSpecifier) logic as-is; also add an integration test that pages through >1 page of identical-price or identical-likeCount products using the repository/pagination (pageable) to assert no duplicates or missing items across pages.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java-35-37 (1)
35-37:⚠️ Potential issue | 🟠 Major정렬 파라미터의 표준화된 에러 처리 패턴 적용 필요.
Line 35의 enum 직접 바인딩은 현재 프레임워크 예외로 처리되어 표준 에러 응답이 반환되므로 기능적으로는 문제 없다. 그러나 프로젝트의 통일된 에러 처리 패턴(모든 에러는 CoreException → ApiControllerAdvice로 흐르기)을 따르기 위해, 문자열로 받은 뒤 명시적으로 파싱하고 실패 시
CoreException(ErrorType.BAD_REQUEST)를 던지도록 수정하는 것이 바람직하다.수정안
public ApiResponse<PageResponse<ProductV1Dto.ProductResponse>> getProducts( `@RequestParam`(required = false) Long brandId, - `@RequestParam`(defaultValue = "LATEST") ProductSort sort, + `@RequestParam`(defaultValue = "LATEST") String sort, `@PageableDefault`(size = 20) Pageable pageable ) { - Page<ProductV1Dto.ProductResponse> page = productFacade.getActiveProducts(brandId, sort, pageable) + ProductSort productSort; + try { + productSort = ProductSort.valueOf(sort.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new CoreException(ErrorType.BAD_REQUEST, "지원하지 않는 정렬 조건입니다."); + } + + Page<ProductV1Dto.ProductResponse> page = productFacade.getActiveProducts(brandId, productSort, pageable) .map(ProductV1Dto.ProductResponse::from);E2E 테스트에
GET /api/v1/products?sort=INVALID케이스를 추가하여 400 상태코드와 표준 에러 바디 포맷을 검증한다.🤖 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/product/ProductV1Controller.java` around lines 35 - 37, The ProductV1Controller currently binds ProductSort directly from the request; change the controller method signature to accept the sort as a String (e.g., sortParam) and then explicitly parse it to ProductSort inside the method (use ProductSort.valueOf or a safe parse helper); on parse failure catch IllegalArgumentException (or null/blank) and throw new CoreException(ErrorType.BAD_REQUEST) so the error flows through ApiControllerAdvice and returns the standardized error body; keep the `@PageableDefault` Pageable pageable param unchanged and ensure all code that used the original ProductSort variable now uses the parsed enum.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java-10-10 (1)
10-10:⚠️ Potential issue | 🟠 Major
@Valid및 DTO 검증 어노테이션을 제거하고 도메인 계층 검증으로 통일하다현재 코드는
@Valid어노테이션과 DTO의@NotBlank,@NotNull검증을 함께 사용하는데, 이는 프로젝트 아키텍처 원칙(헥사고날 아키텍처)에 맞지 않는다. 프로젝트의 설계상 모든 검증은 도메인 계층(Product 엔티티 생성자)에서만 수행되어야 하며, 이를 통해 어떤 진입점에서든 검증 규칙이 강제되고 복잡한 도메인 규칙을 안전하게 표현할 수 있다. 현재 DTO 검증 어노테이션을 제거하고@Valid를 삭제하면 검증 책임이 도메인 계층으로 명확히 통합되며, Product.create()가 CoreException으로 모든 오류를 일원화한다. 추가 테스트로 잘못된 상품 요청(name 필드 누락, price 음수 등)에 대해 통합 테스트를 작성하여 항상 도메인 검증에서 CoreException이 발생하는지 검증이 필요하다.🤖 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/product/AdminProductV1Controller.java` at line 10, 현재 컨트롤러와 DTO에서 사용 중인 Jakarta Bean Validation 어노테이션을 제거하여 검증 책임을 도메인으로 일원화하세요: AdminProductV1Controller에서 import jakarta.validation.Valid와 메서드 파라미터의 `@Valid` 제거하고, 이 컨트롤러에서 사용하는 요청 DTO들(상품 생성/수정 요청 DTO)에 붙어 있는 `@NotBlank`, `@NotNull`, `@Positive` 등 모든 javax/jakarta validation 어노테이션을 삭제하여 DTO는 단순 데이터 전달 객체로 만드십시오; 대신 도메인(Product.create(...) 등)에서 모든 검증을 수행해 CoreException을 던지도록 보장하고, 관련 통합 테스트를 추가/수정하여 name 누락, 음수 price 등 잘못된 요청이 항상 도메인에서 CoreException을 발생시키는지 확인하세요.apps/commerce-api/src/main/java/com/loopers/interfaces/api/auth/LoginUserArgumentResolver.java-18-23 (1)
18-23:⚠️ Potential issue | 🟠 MajorNull 처리 누락으로 인한 NPE 가능성
resolveArgument메서드에서webRequest.getNativeRequest(HttpServletRequest.class)가null을 반환할 수 있고,request.getAttribute("userId")도 인터셉터가 값을 설정하지 않았거나 제외 경로에서 호출된 경우null이 될 수 있다.
null이 컨트롤러로 전달되면 후속 로직에서 NPE가 발생할 수 있으며, 이는 운영 환경에서 500 에러로 나타난다. 방어적 코딩으로 명확한 예외를 발생시켜 문제 원인을 빠르게 파악할 수 있도록 해야 한다.🛡️ 방어적 null 처리 제안
`@Override` public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + if (request == null) { + throw new CoreException(ErrorType.UNAUTHORIZED, "HttpServletRequest를 가져올 수 없습니다."); + } - return request.getAttribute("userId"); + Object userId = request.getAttribute("userId"); + if (userId == null) { + throw new CoreException(ErrorType.UNAUTHORIZED, "인증 정보가 존재하지 않습니다."); + } + return 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/interfaces/api/auth/LoginUserArgumentResolver.java` around lines 18 - 23, resolveArgument currently dereferences webRequest.getNativeRequest(HttpServletRequest.class) and returns request.getAttribute("userId") without null checks, which can cause NPEs or opaque 500 errors; update resolveArgument to validate both the HttpServletRequest (result of getNativeRequest) and the "userId" attribute and, if either is null, throw a clear runtime exception (e.g., IllegalStateException) with a message like "HttpServletRequest missing" or "userId attribute not set - authentication interceptor did not run" so callers get an explicit failure instead of an NPE; apply these checks inside the resolveArgument method (referencing resolveArgument, webRequest.getNativeRequest(HttpServletRequest.class), and request.getAttribute("userId")).apps/commerce-api/src/test/java/com/loopers/application/order/OrderApplicationServiceTest.java-34-67 (1)
34-67:⚠️ Potential issue | 🟠 MajorvalidateItems에서 수량 검증이 누락되어 있다
현재 구현에서
validateItems메서드는 빈 목록과 중복 상품만 검증하고 있으며, 각 주문 항목의 수량이 0 이하인 경우에 대한 검증이 없다. HTTP 요청 DTO 레벨의@Min(1)검증만으로는 부족하다. 서비스 계층에서 수량 검증이 누락되면 비정상적인 데이터가 주문 생성까지 진행되어 데이터 무결성이 깨질 수 있다.
validateItems메서드에 다음 검증을 추가해야 한다:
- 각 항목의 수량이 1 이상인지 확인
그리고 테스트에 다음 케이스를 추가해야 한다:
- 수량이 0인 경우 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/application/order/OrderApplicationServiceTest.java` around lines 34 - 67, Add quantity validation to orderService.validateItems so it throws a CoreException with ErrorType.BAD_REQUEST when any OrderItemCommand has quantity < 1; update the implementation of validateItems to iterate items and check item.getQuantity() (or equivalent) and raise the same BAD_REQUEST used for empty/duplicate checks. Also add two unit tests in OrderApplicationServiceTest.ValidateItems: one that constructs items with a quantity of 0 and asserts CoreException with ErrorType.BAD_REQUEST, and another with a negative quantity (e.g., -1) asserting the same error, mirroring the style of the existing duplicate/empty tests.apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemRepositoryImpl.java-21-23 (1)
21-23:⚠️ Potential issue | 🟠 Major
findByOrderId결과에 정렬이 명시되지 않아 주문 항목 조회 순서가 비결정적이다현재
OrderItemJpaRepository.findByOrderId는 ORDER BY 절을 포함하지 않는다. 테스트 코드(OrderV1ApiE2ETest:269, AdminOrderV1ApiE2ETest:152)에서.get(0)으로 첫 번째 아이템에 접근하는 패턴이 있어 실제로 순서에 의존하고 있다. 데이터베이스 플랜, 인덱스 상태, 통계 갱신 등에 따라 동일 주문의 아이템 반환 순서가 달라질 수 있으며, 이는 API 응답 일관성과 테스트 신뢰도를 훼손한다.
OrderItemJpaRepository에 정렬을 명시한 메서드(예:findByOrderIdOrderByIdAsc)를 추가하고OrderItemRepositoryImpl에서 해당 메서드를 호출하도록 수정해야 한다.수정 예시
`@Override` public List<OrderItem> findByOrderId(Long orderId) { - return orderItemJpaRepository.findByOrderId(orderId); + return orderItemJpaRepository.findByOrderIdOrderByIdAsc(orderId); }// apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemJpaRepository.java List<OrderItem> findByOrderIdOrderByIdAsc(Long 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/infrastructure/order/OrderItemRepositoryImpl.java` around lines 21 - 23, The current OrderItemRepositoryImpl.findByOrderId uses orderItemJpaRepository.findByOrderId which does not guarantee deterministic ordering; update the repository to call a sorted query by adding a method on OrderItemJpaRepository like findByOrderIdOrderByIdAsc(Long orderId) and change OrderItemRepositoryImpl.findByOrderId to invoke orderItemJpaRepository.findByOrderIdOrderByIdAsc(orderId) so returned items are consistently ordered (e.g., by id ascending) to stabilize tests that access .get(0).apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java-38-40 (1)
38-40:⚠️ Potential issue | 🟠 Major브랜드 삭제 시 soft-delete된 상품의 좋아요가 누락될 수 있다.
운영 관점에서 현재 구현은
deletedAtIsNull상품 ID만 대상으로 좋아요를 삭제하므로, 이미 soft-delete된 상품에 남은 좋아요 레코드가 잔존해 통계 왜곡 및 데이터 정합성 이슈가 발생할 수 있다.
수정안으로 브랜드 기준 “전체 상품 ID(삭제 여부 무관)”를 조회하는 경로를 별도로 두고, 그 전체 ID로likeService.deleteAllByProductIds(...)를 수행한 뒤 상품/브랜드 삭제를 진행해야 한다.
추가 테스트로 “좋아요가 있는 상품을 먼저 soft-delete한 후 브랜드 삭제” 시 해당 좋아요가 반드시 hard-delete되는 케이스를 추가해야 한다.🤖 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/BrandFacade.java` around lines 38 - 40, The current flow uses productService.getProductIdsByBrandId(brandId) which returns only non-deleted products and so misses likes belonging to soft-deleted products; change the call to retrieve all product IDs regardless of deletedAt (e.g. add/use productService.getAllProductIdsByBrandId(brandId) or similar) and pass that full list into likeService.deleteAllByProductIds(productIds) before calling productService.deleteAllByBrandId(brandId); also add a test that soft-deletes a product with likes first, then deletes the brand, asserting that likes for that product are hard-deleted.apps/commerce-api/src/main/java/com/loopers/application/user/UserApplicationService.java-43-56 (1)
43-56:⚠️ Potential issue | 🟠 Major인증 실패 메시지가 계정 존재 여부를 노출한다.
운영 관점에서 로그인 ID 미존재와 비밀번호 불일치가 다른 메시지로 관측되면 계정 열거 공격에 악용될 수 있다.
수정안으로authenticate내부에서 사용자 조회 실패와 비밀번호 불일치를 동일한CoreException(동일 코드/동일 메시지)으로 통합하고,loginId를 포함한 상세 식별자는 외부 응답에 노출하지 않아야 한다.
추가 테스트로 “없는 loginId”와 “틀린 비밀번호” 두 케이스가 완전히 동일한 에러 타입/메시지를 반환하는지 검증해야 한다.🤖 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/user/UserApplicationService.java` around lines 43 - 56, The current flow exposes whether a loginId exists via getUserByLoginId's detailed CoreException; change logic so authenticate is the single place that throws a generic, identical CoreException for both missing account and bad password: modify getUserByLoginId to return an Optional<User> (or let it propagate but don't include loginId in its message) and in authenticate, first attempt to retrieve the user via userRepository.findByLoginId(loginId).orElse(null) (or Optional#orElse), then if user is null or passwordEncoder.matches(loginPw, user.getPassword()) is false, throw new CoreException(ErrorType.NOT_FOUND, "사용자 정보가 올바르지 않습니다."); remove any loginId-containing messages from thrown exceptions, and add tests asserting that both "missing loginId" and "wrong password" scenarios produce the exact same ErrorType and message.apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/AdminBrandV1Controller.java-34-34 (1)
34-34:⚠️ Potential issue | 🟠 Majorldap 헤더가 수신되지만 사용되지 않는다
X-Loopers-Ldap헤더가 모든 엔드포인트에서 수신되지만 비즈니스 로직에서 활용되지 않는다. 보안 관점에서 관리자 API는 LDAP 인증/인가 검증이 필요할 수 있으며, 감사(audit) 로그에도 기록되어야 한다.
- 인가가 목적이라면: 인터셉터 또는 서비스 레이어에서 LDAP 검증 로직 추가 필요
- 감사가 목적이라면: 누가 어떤 작업을 했는지 로그 기록 필요
Also applies to: 44-44, 54-54, 63-63, 73-73
🤖 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/brand/AdminBrandV1Controller.java` at line 34, The X-Loopers-Ldap header is accepted in AdminBrandV1Controller methods as the parameter ldap but never used; update the controller to either (a) delegate ldap validation to a centralized component by registering an interceptor that invokes a new LdapAuthService.validate(String ldap) before controller handlers run, or (b) within each handler call an injected LdapAuthService.validate(ldap) and an AuditService.record(actor=ldap, action="brand.*", details=...) to record who performed the action; ensure you reference the existing parameter name ldap and the controller class AdminBrandV1Controller and add unit tests for both auth failure and audit logging scenarios.apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java-29-32 (1)
29-32:⚠️ Potential issue | 🟠 Major
findByUserIdAndCreatedAtBetween메서드 정렬 순서를 JPA Repository에 명시적으로 지정해야 한다.
InMemoryOrderRepository에서는 결과를 ID 역순으로 정렬하지만(sorted(Comparator.comparing(Order::getId).reversed())),OrderJpaRepository의 같은 메서드는 정렬 순서를 지정하지 않는다. 이는 테스트 환경과 실제 운영 환경에서 반환 순서가 달라지므로, 사용자 UI에 표시되는 주문 목록의 순서가 환경마다 다르게 나타난다. 또한 테스트는 통과하나 운영 환경에서 요구사항 불일치가 발생할 수 있다.
OrderJpaRepository.findByUserIdAndCreatedAtBetween메서드를@Query어노테이션으로 명시적 정렬을 추가하거나,OrderRepositoryImpl에서 반환 전에 정렬을 수행하도록 수정한다. 수정 후 정렬 순서가 일관되는지 확인하는 통합 테스트를 작성한다.🤖 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/OrderRepositoryImpl.java` around lines 29 - 32, The OrderRepositoryImpl currently delegates to OrderJpaRepository.findByUserIdAndCreatedAtBetween without enforcing a sort, which causes inconsistent ordering vs InMemoryOrderRepository (which sorts by id descending); update the implementation so results are consistently ordered by id descending by either updating OrderJpaRepository.findByUserIdAndCreatedAtBetween (add an `@Query` with ORDER BY o.id DESC or change method signature to include Sort) or, if you prefer to keep the repository unchanged, sort the returned List in OrderRepositoryImpl before returning (use Comparator.comparing(Order::getId).reversed()); then add an integration test verifying the returned order is descending by id for findByUserIdAndCreatedAtBetween.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java-38-38 (1)
38-38:⚠️ Potential issue | 🟠 MajorBean Validation 어노테이션을 DTO에서 제거하고 도메인 레이어로 이동해야 한다
OrderV1Dto.CreateRequest와OrderItemRequest가@NotNull,@NotEmpty,@Min등의 Bean Validation 어노테이션을 포함하고 있다(lines 15, 19-20). 프로젝트는 DTO에 검증 어노테이션을 사용하지 않고 모든 검증을 도메인 레이어에서 수행하는 정책이다.현재 상태에서
@Valid를 통해 검증이 실행되면MethodArgumentNotValidException이 발생하여CoreException → ApiControllerAdvice표준 에러 처리 패턴을 우회하게 되어 일관성 없는 에러 응답이 발생한다. DTO의 모든 Bean Validation 어노테이션을 제거하고,Order엔티티 생성자 또는 서비스 레이어에서 검증 로직을 구현해야 한다. 이후@Valid도 제거한다.🤖 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/OrderV1Controller.java` at line 38, Remove all Bean Validation annotations (`@NotNull`, `@NotEmpty`, `@Min`, etc.) from OrderV1Dto.CreateRequest and nested OrderItemRequest and remove the `@Valid` on the controller parameter in OrderV1Controller (the method accepting OrderV1Dto.CreateRequest). Implement the equivalent validation inside the domain layer instead — e.g., add validation checks in the Order entity constructor/factory or in the OrderService create method and throw the project's CoreException on invalid input so the ApiControllerAdvice handles errors consistently.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java-70-73 (1)
70-73:⚠️ Potential issue | 🟠 Major날짜 파싱 실패 시 500 에러 발생 가능
ZonedDateTime.parse가 실패하면DateTimeParseException이 발생하여 500 에러가 반환된다. 운영 환경에서 클라이언트의 잘못된 입력으로 인한 500 에러는 모니터링 노이즈를 유발하고, 장애 알람 임계치에 영향을 줄 수 있다.🐛 CoreException으로 감싸는 수정안
private ZonedDateTime parseZonedDateTime(String value) { + try { String normalized = value.replace(" ", "+"); return ZonedDateTime.parse(normalized, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } catch (DateTimeParseException e) { + throw new CoreException(ErrorType.BAD_REQUEST, "날짜 형식이 올바르지 않습니다: " + value, e); + } }추가로
import java.time.format.DateTimeParseException;과import com.loopers.support.error.CoreException;,import com.loopers.support.error.ErrorType;이 필요하다.🤖 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/OrderV1Controller.java` around lines 70 - 73, The parseZonedDateTime method currently lets ZonedDateTime.parse throw DateTimeParseException which results in 500s; catch java.time.format.DateTimeParseException inside parseZonedDateTime and rethrow a com.loopers.support.error.CoreException with an appropriate ErrorType (e.g., ErrorType.INVALID_ARGUMENT or a BAD_REQUEST-equivalent) including the original exception message as cause; add the necessary imports for DateTimeParseException, CoreException, and ErrorType and ensure the thrown CoreException results in a client-facing 4xx instead of a 500.
| @@ -0,0 +1,14 @@ | |||
| package com.loopers.application.order; | |||
|
|
|||
| import com.loopers.interfaces.api.order.OrderV1Dto; | |||
There was a problem hiding this comment.
application 계층이 interfaces DTO를 참조해 의존 방향이 역전된다
운영 관점에서 이 결합은 어댑터 교체(REST 외 배치/메시지 진입점) 시 코어 레이어 재컴파일과 광범위한 파급을 만든다. 수정안으로 OrderCreateCommand는 API DTO를 모르도록 유지하고, OrderV1Dto.CreateRequest -> OrderCreateCommand 매핑은 컨트롤러/파사드(interfaces 계층)로 이동해야 한다. 추가 테스트로 ArchUnit 규칙(com.loopers.application..가 com.loopers.interfaces..에 의존 금지)을 넣어 재발을 차단해야 한다.
Also applies to: 8-13
🤖 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/OrderCreateCommand.java`
at line 3, OrderCreateCommand currently depends on the API DTO (remove import
com.loopers.interfaces.api.order.OrderV1Dto) causing an inverted dependency;
change it so OrderCreateCommand is a plain application-layer command (no
references to OrderV1Dto or its nested CreateRequest) and move the mapping logic
from OrderV1Dto.CreateRequest -> OrderCreateCommand into the controller/facade
in the interfaces layer where HTTP/API DTOs are handled; update
controller/facade to construct OrderCreateCommand from the incoming
OrderV1Dto.CreateRequest, remove DTO references from OrderCreateCommand, and add
an ArchUnit test enforcing that com.loopers.application.. must not depend on
com.loopers.interfaces...
| public void decreaseStock(int quantity) { | ||
| if (this.stockQuantity < quantity) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."); | ||
| } | ||
| this.stockQuantity -= quantity; | ||
| } |
There was a problem hiding this comment.
재고 차감에서 0/음수 수량이 허용되어 재고가 증가할 수 있다
운영 관점에서 잘못된 수량이 들어오면 재고 정합성이 깨져 주문 가능 수량과 실제 재고가 불일치하게 된다. 수정안으로 quantity <= 0을 선제 차단하고 부족 재고 검증을 수행해야 한다.
수정 예시
public void decreaseStock(int quantity) {
+ if (quantity <= 0) {
+ throw new CoreException(ErrorType.BAD_REQUEST, "차감 수량은 1 이상이어야 합니다.");
+ }
if (this.stockQuantity < quantity) {
throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다.");
}
this.stockQuantity -= quantity;
}추가 테스트로 decreaseStock(0), decreaseStock(-1) 예외와 정상 차감 케이스를 함께 검증해야 한다.
📝 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 void decreaseStock(int quantity) { | |
| if (this.stockQuantity < quantity) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."); | |
| } | |
| this.stockQuantity -= quantity; | |
| } | |
| public void decreaseStock(int quantity) { | |
| if (quantity <= 0) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "차감 수량은 1 이상이어야 합니다."); | |
| } | |
| if (this.stockQuantity < quantity) { | |
| throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."); | |
| } | |
| this.stockQuantity -= 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 86 - 91, The decreaseStock method allows zero/negative quantities
which can corrupt stock; update Product.decreaseStock to first validate the
input (if quantity <= 0) and throw a CoreException(ErrorType.BAD_REQUEST, "유효하지
않은 수량입니다." or similar), then perform the existing insufficient-stock check (if
this.stockQuantity < quantity) and subtract only after both validations; add
unit tests covering decreaseStock(0), decreaseStock(-1) raising the BAD_REQUEST
exception and a normal successful decrease case to verify correct subtraction of
stockQuantity.
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find apps/commerce-api/src/main/java/com/loopers/domain -name "*Repository*.java" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 638
🏁 Script executed:
if [ -f "apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java" ]; then
echo "=== ProductRepository.java 파일 존재 ===" && wc -l "apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java"
fiRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 206
🏁 Script executed:
cat -n "apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 864
🏁 Script executed:
rg -n "^import org\.springframework" apps/commerce-api/src/main/java/com/loopers/domain/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1130
도메인 계층의 Repository 인터페이스가 Spring 프레임워크 타입에 직접 의존해 기술 중립성을 위반한다
domain 패키지에서 Spring의 Page<T>와 Pageable 타입을 노출하면 도메인 계층이 특정 프레임워크에 묶인다. 이로 인해 추후 JPA 외 다른 저장소 기술로 전환하거나, 도메인 로직을 다른 모듈에서 재사용할 때 Spring 의존성을 함께 끌어안게 되어 변경 파급 범위가 커진다. 또한 도메인 계층을 단독으로 테스트하거나 다른 애플리케이션과 공유하기 어려워진다.
도메인/애플리케이션 계층 전용 페이지네이션 모델(PageRequest, PageResult 등)을 정의하고, Page<T>와 Pageable에 대한 변환 로직은 infrastructure 구현체에서만 처리해야 한다. infrastructure 계층의 JPA Repository 구현체에서 도메인 인터페이스의 반환값으로 변환할 때 Spring 타입을 사용하도록 격리한다.
추가로 ArchUnit을 도입해 com.loopers.domain.. 패키지에서 org.springframework.. import를 방지하는 아키텍처 규칙을 자동화하면, 향후 유사한 계층 침해를 조기에 감지할 수 있다.
🤖 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)가 Spring
타입(org.springframework.data.domain.Page, Pageable)에 직접 의존하고 있으니 도메인 전용 페이지네이션
모델(e.g. domain.pagination.PageRequest, domain.pagination.PageResult)을 만든 뒤
ProductRepository의 메서드 시그니처를 해당 도메인 타입으로 교체하고, 기존 Spring의 Page/Pageable 변환 로직은
infrastructure 계층의 JPA 구현체(예: ProductRepositoryJpaImpl)로 이동하여 변환해 반환하도록 구현하세요;
추가로 com.loopers.domain.. 패키지에서 org.springframework.. import를 방지하는 ArchUnit 규칙을
도입해 동일한 침해가 재발하지 않도록 설정하세요.
| @GetMapping | ||
| public ApiResponse<PageResponse<OrderV1Dto.OrderResponse>> getOrders( | ||
| @RequestHeader("X-Loopers-Ldap") String ldap, | ||
| @PageableDefault(size = 20) Pageable pageable | ||
| ) { | ||
| Page<OrderV1Dto.OrderResponse> page = orderService.getAllOrders(pageable) | ||
| .map(OrderV1Dto.OrderResponse::from); | ||
| return ApiResponse.success(PageResponse.from(page)); | ||
| } | ||
|
|
||
| @GetMapping("/{orderId}") | ||
| public ApiResponse<OrderV1Dto.OrderDetailResponse> getOrder( | ||
| @RequestHeader("X-Loopers-Ldap") String ldap, | ||
| @PathVariable Long orderId |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
echo "[1] WebConfig 인터셉터 설정 확인"
find apps/commerce-api/src -name "WebConfig.java" -o -name "*Config.java" | head -10
echo ""
echo "[2] 관리자 경로 인터셉터 매핑"
rg -n "addInterceptors|api-admin|LoginUserInterceptor" apps/commerce-api/src/main/java --type java -A 5 -B 2 | head -50
echo ""
echo "[3] 컨트롤러 인증 파라미터 방식 - 다른 Admin 컨트롤러 확인"
find apps/commerce-api/src -name "*AdminController.java" -o -name "*Controller.java" | grep -i admin | head -5
echo ""
echo "[4] RequestHeader 사용 현황"
rg -n "@RequestHeader.*X-Loopers-Ldap" apps/commerce-api/src/main/java --type java | head -20
echo ""
echo "[5] LoginUserArgumentResolver 또는 `@LoginUser` 패턴 확인"
rg -n "LoginUserArgumentResolver|@LoginUser" apps/commerce-api/src/main/java --type java | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 9541
🏁 Script executed:
#!/bin/bash
set -e
echo "[1] WebConfig 전체 내용 확인"
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/config/WebConfig.java
echo ""
echo "[2] AdminOrderV1Controller 전체 내용 확인"
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/AdminOrderV1Controller.java
echo ""
echo "[3] AdminBrandV1Controller 전체 내용 확인"
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/AdminBrandV1Controller.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 7895
🏁 Script executed:
#!/bin/bash
set -e
echo "[1] Admin 관련 인증 설정 확인 (필터/인터셉터)"
find apps/commerce-api/src/main/java -name "*Admin*" -type f | xargs grep -l "Interceptor\|Filter\|Security" 2>/dev/null || echo "No admin-specific auth found"
echo ""
echo "[2] SecurityConfig 또는 인증 관련 설정 확인"
find apps/commerce-api/src -name "*Security*" -o -name "*Auth*" | grep -E "\.java$" | head -10
echo ""
echo "[3] LoginUserInterceptor 구현 확인 - 어떤 경로를 처리하는지"
cat apps/commerce-api/src/main/java/com/loopers/interfaces/api/auth/LoginUserInterceptor.java
echo ""
echo "[4] `@RequestHeader` ldap 파라미터가 실제로 사용되는지 다시 확인"
rg -A 10 "@RequestHeader.*X-Loopers-Ldap" apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/AdminOrderV1Controller.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2257
관리자 API가 인증 계층 없이 노출되어 있다
/api-admin/v1/** 경로는 WebConfig의 LoginUserInterceptor 등록 범위(/api/v1/**)에서 완전히 제외되어 있다. @RequestHeader("X-Loopers-Ldap") String ldap 파라미터는 메서드 내에서 사용되지 않으며, 어떤 검증도 수행되지 않는다. 현재 상태로는 외부 L4/Nginx 검증이 실패해도 관리자 API는 보호받지 못한다.
LoginUserInterceptor를 /api-admin/**로 확장하거나, 관리자용 별도 인증 메커니즘을 추가해야 한다. 권장 방식은 /api/v1/**의 @LoginUser Long userId 패턴을 관리자 엔드포인트에도 적용하되, 권한 검증을 @PreAuthorize 또는 인터셉터 단계에서 강제하는 것이다. 사용하지 않는 ldap 파라미터는 제거하거나, 실제 검증과 감사(audit) 로깅에 사용하도록 개선한다.
🤖 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/AdminOrderV1Controller.java`
around lines 27 - 40, AdminOrderV1Controller exposes admin endpoints without
authentication: remove the unused `@RequestHeader`("X-Loopers-Ldap") String ldap
from getOrders/getOrder and enforce authentication/authorization either by
expanding LoginUserInterceptor registration to include /api-admin/** in
WebConfig or by adding the existing `@LoginUser` Long userId parameter to these
controller methods and protecting them with `@PreAuthorize` checks (or an
admin-specific interceptor); update audit/logging to use ldap only if you
implement real validation.
There was a problem hiding this comment.
Actionable comments posted: 3
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/SignUpV1Controller.java (1)
4-25:⚠️ Potential issue | 🟠 Major컨트롤러의
@Valid는 프로젝트의 오류 처리 단일 경로와 충돌할 수 있다.Line 25의
@Valid는MethodArgumentNotValidException분기를 열어CoreException -> ApiControllerAdvice표준 응답 포맷 일관성을 깨뜨릴 수 있다.
수정안으로@Valid와jakarta.validation.Validimport를 제거하고, DTO→Command 변환 후 도메인/서비스 검증만 사용해 오류 경로를 단일화하는 것이 좋다.
추가 테스트로 잘못된 회원가입 입력(예: 잘못된 이메일/형식) 시에도 에러 응답이 항상 동일한ApiResponse포맷으로 내려오는 E2E 테스트를 권장한다.수정 예시
-import jakarta.validation.Valid; ... - public ApiResponse<Void> signUp(`@Valid` `@RequestBody` UserV1Dto.CreateRequest request) { + public ApiResponse<Void> signUp(`@RequestBody` UserV1Dto.CreateRequest request) {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..." 및 "enforce unified error handling by routing errors through CoreException to ApiControllerAdvice... Do not introduce MethodArgumentNotValidException handlers or Bean Validation handling."
🤖 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/SignUpV1Controller.java` around lines 4 - 25, The controller currently uses jakarta.validation.@Valid on the signUp endpoint which opens MethodArgumentNotValidException and bypasses the project's unified CoreException -> ApiControllerAdvice flow; remove the `@Valid` annotation and the jakarta.validation.Valid import from SignUpV1Controller, update the signUp method signature to accept UserV1Dto.CreateRequest without `@Valid`, and ensure any validation is moved into the domain/service layer (e.g., convert UserV1Dto.CreateRequest to a SignUpCommand within SignUpV1Controller and have SignUpService or the domain throw CoreException on validation failures) so all errors flow through SignUpV1Controller.signUp -> SignUpService and adhere to ApiResponse formatting.apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java (1)
172-199:⚠️ Potential issue | 🟡 Minor테스트 메서드명과 기대 상태코드가 불일치한다.
Line 174의 메서드명은
BadRequest를 가리키지만 실제 검증은 Line 199에서NOT_FOUND를 기대하여, 장애 분석 시 테스트 실패 원인 해석을 혼동시킬 수 있다.
수정안으로 메서드명을 기대 결과에 맞게returnsNotFound_whenCurrentPasswordNotMatches로 정렬하는 것이 좋다.
추가 테스트로 상태코드-메서드명 규칙을 강제하는 테스트 네이밍 규칙 점검(예: ArchUnit/커스텀 규칙) 도입을 권장한다.수정 예시
- void returnsBadRequest_whenCurrentPasswordNotMatches() { + void returnsNotFound_whenCurrentPasswordNotMatches() {🤖 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/UserV1ApiE2ETest.java` around lines 172 - 199, Rename the test method returnsBadRequest_whenCurrentPasswordNotMatches to returnsNotFound_whenCurrentPasswordNotMatches so the method name matches the asserted expectation (HttpStatus.NOT_FOUND) in the test; update any references or annotations that call this method (e.g., the `@Test` method in UserV1ApiE2ETest) to use the new name to avoid confusion during test failure analysis.
♻️ Duplicate comments (1)
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java (1)
3-4:⚠️ Potential issue | 🟠 Major도메인 Repository가 Spring
Page/Pageable에 직접 의존해 계층 경계를 침범한다Line 3-4와 Line 12-13에서 프레임워크 타입이 도메인 계약에 노출되어 운영 중 저장소 기술 변경 시 파급 범위가 커지고, 도메인 단독 테스트/재사용성이 떨어진다. 도메인 전용 페이지 모델(
PageQuery,PageResult)로 인터페이스를 치환하고, Spring 타입 변환은 infrastructure 구현체로 격리해야 한다. 추가로com.loopers.domain..에서org.springframework..import를 금지하는 ArchUnit 테스트를 추가해 재발을 차단해야 한다.As per coding guidelines "/domain//*.java: 도메인 규칙과 인프라 관심사가 섞이면 분리하도록 제안한다."
Also applies to: 12-13
🤖 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 exposes framework types org.springframework.data.domain.Page and Pageable in its domain interface (imports on Page/Pageable and methods using them); replace those references with domain-level pagination contracts such as PageQuery (request) and PageResult<T> (response), move any conversion between PageQuery/PageResult and Spring Page/Pageable into the infrastructure implementation of ProductRepository, and add an ArchUnit rule that forbids imports of org.springframework.. within com.loopers.domain.. to prevent regressions.
🟠 Major comments (26)
apps/commerce-api/src/main/java/com/loopers/application/order/OrderCreateCommand.java-5-6 (1)
5-6:⚠️ Potential issue | 🟠 Major
items방어적 복사와 입력 가드가 없어 주문 커맨드가 생성 후 변조될 수 있다.운영에서 검증 직후 원본 리스트가 바뀌면 주문 품목이 의도와 다르게 저장되어 데이터 정합성 장애로 이어질 수 있다. 레코드 compact constructor에서
userId/items를 즉시 검증하고List.copyOf(items)로 불변화하는 수정이 필요하다. 추가 테스트로는 (1) 생성 후 원본 리스트 변경 시command.items()가 변하지 않는지, (2)null/empty items,null userId입력이 일관된 BAD_REQUEST로 실패하는지를 넣어야 한다.As per coding guidelines, `**/*.java`에서는 "null 처리, 방어적 복사, 불변성"을 점검해야 한다. Based on learnings, 오류 처리는 CoreException으로 일원화해야 일관된 응답 포맷을 유지할 수 있다.제안 수정안
package com.loopers.application.order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; import java.util.List; public record OrderCreateCommand(Long userId, List<OrderItemCommand> items) { + public OrderCreateCommand { + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 필수값이다."); + } + if (items == null || items.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 상품은 최소 1개 이상이어야 한다."); + } + items = 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/main/java/com/loopers/application/order/OrderCreateCommand.java` around lines 5 - 6, OrderCreateCommand currently accepts a mutable items list and lacks input guards; update the record's compact constructor in OrderCreateCommand to validate userId != null and items != null && !items.isEmpty(), wrap the items field with List.copyOf(items) to make it immutable, and throw the unified CoreException on invalid inputs; then add tests that (1) mutate the original source list after construction and assert command.items() is unchanged, and (2) assert constructions with null userId, null items, and empty items each fail with a BAD_REQUEST CoreException.apps/commerce-api/src/main/java/com/loopers/application/user/SignUpService.java-18-27 (1)
18-27:⚠️ Potential issue | 🟠 Major회원가입 중복 검사에 TOCTOU 경쟁 조건이 있다.
Line 18-20의 사전 조회 후 Line 26 저장 방식은 동시 요청에서 둘 다 통과할 수 있어, 운영에서는 간헐적 중복 삽입 시도 또는 DB 예외 노출로 이어질 수 있다.
수정안으로 DB 유니크 제약 위반을CoreException(CONFLICT)로 매핑하고 cause를 보존해 일관된 오류 응답과 추적 가능성을 확보하는 것이 좋다.
추가 테스트로 동일loginId동시 가입(예: CountDownLatch)에서 1건만 성공하고 나머지는 CONFLICT로 귀결되는 통합 테스트를 권장한다.수정 예시
+import org.springframework.dao.DataIntegrityViolationException; ... `@Transactional` public void signUp(SignUpCommand command) { if (userRepository.findByLoginId(command.loginId()).isPresent()) { throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다."); } PasswordPolicyValidator.validate(command.password(), command.birthDate()); String encodedPassword = passwordEncoder.encode(command.password()); User user = User.create(command, encodedPassword); - userRepository.save(user); + try { + userRepository.save(user); + } catch (DataIntegrityViolationException e) { + throw new CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.", e); + } }As per coding guidelines
**/*Service*.java: "멱등성과 중복 처리 방지 전략을 점검한다." 및**/*.java: "예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다."🤖 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/user/SignUpService.java` around lines 18 - 27, The pre-check using userRepository.findByLoginId(...) followed by userRepository.save(user) has a TOCTOU race; instead wrap the save to catch database unique-constraint/duplicate-key exceptions (thrown by userRepository.save or the JPA/DB layer) and rethrow a CoreException(ErrorType.CONFLICT, "이미 존재하는 로그인 ID입니다.", cause) preserving the original exception as the cause and logging a separate detailed message; keep PasswordPolicyValidator.validate, passwordEncoder.encode and User.create as-is, but ensure the save exception handling maps DB uniqueness violations to CONFLICT and rethrows with the original cause, and add an integration test that performs concurrent sign-ups (e.g., CountDownLatch) asserting one success and others receive CONFLICT.apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java-36-43 (1)
36-43:⚠️ Potential issue | 🟠 Major도메인 생성 시 식별자 불변식 검증이 누락되었다.
Line 41에서
userId/productId가 null 또는 0 이하로 들어오면 DB 저장 시점 예외로 늦게 실패하여, 운영에서는 500 응답과 원인 추적 비용 증가로 이어질 수 있다.
수정안으로create또는 생성자에서 식별자 유효성(null/양수)을 즉시 검증해 도메인 경계에서 실패시키는 것이 안전하다.
추가 테스트로Like.create(null, 1L),Like.create(0L, 1L),Like.create(1L, null)케이스의 예외 흐름을 단위 테스트로 고정하는 것이 좋다.수정 예시
+import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; ... private Like(Long userId, Long productId) { + if (userId == null || userId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "유효하지 않은 사용자 ID이다."); + } + if (productId == null || productId <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "유효하지 않은 상품 ID이다."); + } this.userId = userId; this.productId = productId; }As per coding guidelines
**/domain/**/*.java: "엔티티/값 객체/DTO 경계를 명확히 하고, 불변성과 캡슐화를 점검한다."🤖 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 36 - 43, The Like domain factory currently allows invalid identifiers; add null and positive-value validation in the constructor or static factory (e.g., in Like.create and/or private Like(Long userId, Long productId)) to immediately throw IllegalArgumentException with clear messages if userId or productId is null or <= 0, update callers accordingly, and add unit tests asserting exceptions for Like.create(null, 1L), Like.create(0L, 1L), and Like.create(1L, null) to lock the behavior.apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java-14-16 (1)
14-16:⚠️ Potential issue | 🟠 Major@Modifying 삭제 쿼리의 트랜잭션 경계를 리포지토리 메서드 수준에서 명시하면 안전성이 높아진다.
Line 14-16의
deleteByUserIdAndProductId메서드는@Modifying쿼리로 명시적인 트랜잭션 관리가 필수다. 현재는 호출부인LikeService.cancel()과LikeFacade.cancel()에서@Transactional이 선언되어 있어 즉각적 위험은 없지만, 리포지토리 메서드는 public 인터페이스이므로 향후 다른 곳에서 직접 호출될 가능성이 있다. 메서드 수준에서 트랜잭션을 명시하면 호출부의 트랜잭션 설정과 무관하게 안정적으로 동작하며, 리팩터링 시 실수로@Transactional제거나readOnly=true전환으로 인한TransactionRequiredException을 방지할 수 있다.메서드에
@Transactional과@Modifying(clearAutomatically = true, flushAutomatically = true)옵션을 추가하여 영속성 컨텍스트 일관성을 보장하도록 개선한다.🤖 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` around lines 14 - 16, Add method-level transaction semantics to the repository: on LikeJpaRepository's deleteByUserIdAndProductId method, annotate it with `@Transactional` and change `@Modifying` to `@Modifying`(clearAutomatically = true, flushAutomatically = true) so the delete runs in its own transactional boundary and the persistence context is flushed/cleared automatically; ensure the method signature and `@Param` names remain unchanged.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java-33-40 (1)
33-40:⚠️ Potential issue | 🟠 Major도메인 불변식 검증이 수량에만 제한되어 잘못된 주문 항목이 저장될 수 있다.
Line 35-39에서
orderId,productId,productName,unitPrice검증이 없어 null/공백/음수 데이터가 엔티티에 유입될 수 있다. 이 경우 운영에서는 저장 시점에만 실패가 발생해 장애 원인 추적이 지연된다. 생성 시점에CoreException(BAD_REQUEST)로 즉시 차단해야 한다.
수정안은 생성자 진입 시 필수값과 범위를 함께 검증하는 메서드로 확장하는 방식이다.
추가 테스트는orderId null/0,productId null/0,productName null/blank,unitPrice < 0각각에 대해BAD_REQUEST를 검증하면 된다.수정 예시 diff
private OrderItem(Long orderId, Long productId, String productName, long unitPrice, Integer quantity) { - validateQuantity(quantity); + validate(orderId, productId, productName, unitPrice, quantity); this.orderId = orderId; this.productId = productId; this.productName = productName; this.unitPrice = unitPrice; this.quantity = quantity; } +private void validate(Long orderId, Long productId, String productName, long unitPrice, Integer quantity) { + if (orderId == null || orderId < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 ID가 유효하지 않습니다."); + } + if (productId == null || productId < 1) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 ID가 유효하지 않습니다."); + } + if (productName == null || productName.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 필수값입니다."); + } + if (unitPrice < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 단가는 0 이상이어야 합니다."); + } + validateQuantity(quantity); +} + private void validateQuantity(Integer quantity) { if (quantity == null || quantity < 1) { throw new CoreException(ErrorType.BAD_REQUEST, "주문 수량은 1개 이상이어야 합니다."); } }As per coding guidelines
**/*.java: "null 처리 ... 점검한다." 및**/domain/**/*.java: "도메인 규칙과 인프라 관심사가 섞이면 분리하도록 제안한다."Also applies to: 46-49
🤖 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 33 - 40, The constructor OrderItem(Long, Long, String, long, Integer) only validates quantity; add a new private validator (e.g., validateOrderItemFields or validateRequiredFields) and call it from the constructor (and any other constructor/factory overloads such as the one at lines ~46-49) to assert orderId != null && orderId > 0, productId != null && productId > 0, productName != null && not blank, and unitPrice >= 0; on any violation throw CoreException(BAD_REQUEST) so invalid order items are rejected at creation time instead of persisting bad data.apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java-3-4 (1)
3-4: 🛠️ Refactor suggestion | 🟠 Major도메인 저장소 인터페이스가 Spring 타입에 직접 의존해 계층 경계를 위반한다.
domain 계층의 BrandRepository가
Page,Pageable같은 Spring 프레임워크 타입을 직접 import하고 메서드 시그니처에 노출하면, 프레임워크 교체나 테스트 더블 구성 시 도메인 계층까지 영향을 받는다. 현재 OrderRepository, ProductRepository도 동일한 문제를 가진 상태다.도메인 전용 페이지 요청/응답 모델(
DomainPageable,DomainPage등)을 정의하고, SpringPageable변환은 infrastructure 계층의BrandRepositoryImpl에서만 처리하도록 분리해야 한다. 이렇게 하면 InMemoryBrandRepository 같은 테스트 더블도 도메인 계약에만 의존하면 된다.또한 ArchUnit 규칙으로
com.loopers.domain..패키지가org.springframework..import을 금지하는 검증을 추가하면, 유사한 위반을 사전에 방지할 수 있다.🤖 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/BrandRepository.java` around lines 3 - 4, BrandRepository (and similarly OrderRepository, ProductRepository) currently expose Spring types Page and Pageable in the domain layer which couples the domain to Spring; change the domain repository interfaces (e.g., BrandRepository) to use domain-only paging abstractions (e.g., DomainPageable for requests and DomainPage<T> for responses) and remove imports of org.springframework.* from the domain package, then implement conversion between DomainPageable <-> Pageable inside the infrastructure implementation class (BrandRepositoryImpl) where Spring types are allowed; update InMemoryBrandRepository to implement the domain paging contract for tests; finally add an ArchUnit rule to forbid imports of org.springframework.. from com.loopers.domain.. to prevent regressions.apps/commerce-api/src/test/java/com/loopers/domain/brand/InMemoryBrandRepository.java-39-44 (1)
39-44:⚠️ Potential issue | 🟠 Major페이지네이션 계약을 무시해 테스트가 실제 저장소 동작과 어긋난다.
Line 39-44는
Pageable을 받지만 offset/size를 적용하지 않아 항상 전체 데이터를 반환한다. HashMap 기반 저장이므로 정렬 순서도 보장되지 않는다. 이 상태에서는 페이징을 사용한 테스트가 통과해도 운영 DB 조회 결과와 달라 장애를 사전에 탐지하지 못한다.수정안은 deletedAt이 null인 목록을 정렬한 뒤 offset/limit으로 content를 슬라이싱해
PageImpl을 생성하는 것이다.수정 예시 diff
import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.List; `@Override` public Page<Brand> findAllByDeletedAtIsNull(Pageable pageable) { - var list = store.values().stream() - .filter(b -> b.getDeletedAt() == null) - .toList(); - return new PageImpl<>(list, pageable, list.size()); + var filtered = store.values().stream() + .filter(b -> b.getDeletedAt() == null) + .sorted(Comparator.comparing(Brand::getId)) + .toList(); + + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), filtered.size()); + var content = start >= filtered.size() ? List.<Brand>of() : filtered.subList(start, end); + + return new PageImpl<>(content, pageable, filtered.size()); }추가 테스트는
PageRequest.of(0, 1)과PageRequest.of(1, 1)각각에서 content 크기/요소/totalElements를 검증하면 된다.🤖 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/InMemoryBrandRepository.java` around lines 39 - 44, The method findAllByDeletedAtIsNull currently ignores the Pageable and returns the entire unsorted store; update it to (1) collect store.values() filtered by b.getDeletedAt() == null, (2) sort deterministically (e.g., by b.getId() or b.getCreatedAt()) to avoid HashMap order variance, (3) apply pageable.getOffset() and pageable.getPageSize() to slice the sorted list into the page content, and (4) construct new PageImpl<>(pagedContent, pageable, totalFilteredCount). Modify the code in InMemoryBrandRepository (findAllByDeletedAtIsNull) and add tests using PageRequest.of(0,1) and PageRequest.of(1,1) to assert content size/elements and totalElements.apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java-72-74 (1)
72-74:⚠️ Potential issue | 🟠 Major
changeVisibility에 null 방어가 없어 도메인 불변식이 깨질 수 있다운영에서 null이 유입되면 비정상 상태가 영속화 직전까지 유지되고, 최종적으로 서버 오류(500)로 전이되어 사용자 입력 오류를 장애로 처리하게 된다.
수정안은visibility == null을 즉시 차단하고CoreException(BAD_REQUEST)를 반환하는 것이다.
추가 테스트로changeVisibility(null)예외,VISIBLE -> HIDDEN정상 전이,HIDDEN -> VISIBLE복귀 전이를 함께 검증해야 한다.🛡️ 제안 수정안
public void changeVisibility(Visibility visibility) { + if (visibility == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "노출 상태는 필수값입니다."); + } this.visibility = visibility; }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/domain/product/Product.java` around lines 72 - 74, The changeVisibility method in Product must guard against null to preserve domain invariants: update Product.changeVisibility to throw a CoreException with BAD_REQUEST when visibility == null instead of assigning null, and keep the current assignment for non-null values; add unit tests that assert changeVisibility(null) throws the CoreException, that a VISIBLE -> HIDDEN transition succeeds, and that HIDDEN -> VISIBLE transition succeeds to cover normal and edge flows.apps/commerce-api/src/test/java/com/loopers/domain/product/InMemoryProductRepository.java-41-57 (1)
41-57:⚠️ Potential issue | 🟠 Major
findActiveProducts가 페이징/좋아요 정렬 계약을 충족하지 못해 테스트 신뢰도를 떨어뜨린다운영에서 테스트 더블이 실제 저장소 동작과 다르면 정렬/페이징 회귀가 테스트를 통과하고 운영에서만 장애로 드러난다.
수정안은sort == null기본값 처리,LIKES_DESC의likeCount기준 정렬,pageableoffset/size 기반 슬라이싱을 적용하는 것이다.
추가 테스트로 (1)page=1,size=2슬라이싱 정확성, (2)likeCount내림차순 정렬, (3)sort=null기본 정렬 동작을 검증해야 한다.🔧 제안 수정안
`@Override` public Page<Product> findActiveProducts(Long brandId, ProductOrder sort, Pageable pageable) { + ProductOrder effectiveSort = (sort == null) ? ProductOrder.LATEST : sort; + Stream<Product> stream = store.values().stream() .filter(Product::isActive); @@ - Comparator<Product> comparator = switch (sort) { - case PRICE_ASC -> Comparator.comparing(Product::getPrice); - // likes_desc는 Like 도메인 구현 전이므로 인메모리에서 id 기준으로 대체 - case LIKES_DESC, LATEST -> Comparator.comparing(Product::getId).reversed(); + Comparator<Product> comparator = switch (effectiveSort) { + case PRICE_ASC -> Comparator.comparing(Product::getPrice) + .thenComparing(Product::getId); + case LIKES_DESC -> Comparator.comparing(Product::getLikeCount, Comparator.reverseOrder()) + .thenComparing(Product::getId, Comparator.reverseOrder()); + case LATEST -> Comparator.comparing(Product::getId, Comparator.reverseOrder()); }; - List<Product> list = stream.sorted(comparator).toList(); - return new PageImpl<>(list, pageable, list.size()); + List<Product> list = stream.sorted(comparator).toList(); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), list.size()); + List<Product> content = start >= list.size() ? List.of() : list.subList(start, end); + return new PageImpl<>(content, pageable, list.size()); }🤖 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/InMemoryProductRepository.java` around lines 41 - 57, The in-memory findActiveProducts method doesn't honor null sort, doesn't sort LIKES_DESC by actual likeCount, and returns an unsliced Page; fix findActiveProducts to default sort when sort==null (choose LATEST or PRICE_ASC per contract), build the Comparator to handle PRICE_ASC (Product::getPrice), LIKES_DESC (Comparator.comparing(Product::getLikeCount).reversed()), and LATEST (Comparator.comparing(Product::getId).reversed()), collect the filtered/sorted list, compute total = list.size(), then apply pageable slicing using int offset = (int) pageable.getOffset() and int size = pageable.getPageSize(), create subList bounded by offset and Math.min(offset+size,total), and return new PageImpl<>(subList, pageable, total).apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/dto/BrandV1Dto.java-10-20 (1)
10-20:⚠️ Potential issue | 🟠 MajorDTO 계층 Bean Validation 사용을 제거하고 도메인 검증으로 통일해야 한다. 단, BrandV1Dto만이 아닌 프로젝트 전체 DTO의 일관된 수정이 필요하다.
현재 BrandV1Dto의
@NotBlank는실제로 MethodArgumentNotValidException 경로로 분기되어 CoreException 기반의 공통 응답 포맷과 분리된다. 그런데 UserV1Dto, ProductV1Dto, OrderV1Dto도 동일하게@NotBlank,@NotNull,@NotEmpty등의 Bean Validation을 사용 중이고, 모든 컨트롤러에서@Valid를적용하고 있다. 이는 프로젝트 전체가 현재 일관된 Bean Validation 패턴을 따르고 있다는 의미이므로, BrandV1Dto만 수정하면 오히려 일관성이 깨진다.수정 방향은 두 가지 중 선택해야 한다:
- 프로젝트 전체 DTO에서 Bean Validation을 제거하고 도메인 검증으로 통일 (현재 Brand.validateName 패턴처럼)
- 현재 Bean Validation + MethodArgumentNotValidException 핸들링 패턴을 유지
첫 번째 방향을 선택한다면, UserV1Dto, ProductV1Dto, OrderV1Dto의 모든
@NotBlank,@NotNull,@NotEmpty를제거하고 해당 도메인 엔티티(User, Product, Order)의 검증을 강화해야 한다. 또한 컨트롤러의@Valid도제거되어야 한다.🔧 BrandV1Dto 수정안 (첫 번째 방향 선택 시)
import com.loopers.application.brand.BrandInfo; -import jakarta.validation.constraints.NotBlank; import java.time.ZonedDateTime; @@ public record CreateRequest( - `@NotBlank`(message = "브랜드 이름은 필수값입니다.") String name, String description ) {} public record UpdateRequest( - `@NotBlank`(message = "브랜드 이름은 필수값입니다.") String name, String description ) {}추가 테스트로 AdminBrandV1ApiE2ETest에서 name=null, name="", name=" " 케이스에 대해 상태코드뿐 아니라 공통 에러 바디(code/message) 일관성까지 검증해야 한다. 단, 이는 프로젝트 전체 DTO 일관성 확보 후에 수행되어야 한다.
🤖 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/brand/dto/BrandV1Dto.java` around lines 10 - 20, The DTOs (BrandV1Dto CreateRequest/UpdateRequest) currently use Bean Validation annotations (`@NotBlank`) which diverges from the project-wide error handling; choose one approach and apply it consistently — I recommend removing DTO-level Bean Validation and moving validation to the domain (follow Brand.validateName pattern): remove `@NotBlank/`@NotNull/@NotEmpty from BrandV1Dto CreateRequest and UpdateRequest and from all other DTOs (UserV1Dto, ProductV1Dto, OrderV1Dto), remove `@Valid` usage in controllers, and reinforce validation logic inside domain entities (e.g., Brand.validateName, User.validate..., Product.validate..., Order.validate...) so exceptions bubble through the CoreException-based handler; update AdminBrandV1ApiE2ETest to assert unified error body (code/message) for name=null, name="", name=" " cases after changes.apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java-286-295 (1)
286-295:⚠️ Potential issue | 🟠 Major정렬 검증이 누락 데이터를 허용할 수 있어 회귀를 놓칠 수 있다.
운영 관점에서 정렬 API가 일부 상품을 누락해도
indexOf기반 비교는 조건부로 통과할 수 있어 배포 전 탐지가 약해진다. 정렬 검증은 반환 ID를 명시 순서로containsExactly(...)로 검증해 누락과 순서를 동시에 확인해야 한다. 추가 테스트로는 PRICE_ASC/LIKES_DESC 각각에서 예상 3건이 정확히 동일 순서로 반환되는지 검증이 필요하다.수정 예시 diff
- assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(ids.indexOf(cheapProduct.getId())) - .isLessThan(ids.indexOf(middleProduct.getId())) - .isLessThan(ids.indexOf(expensiveProduct.getId())) - ); + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(ids).containsExactly( + cheapProduct.getId(), + middleProduct.getId(), + expensiveProduct.getId() + ) + ); @@ - assertAll( - () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), - () -> assertThat(ids.indexOf(highLikes.getId())) - .isLessThan(ids.indexOf(middleLikes.getId())) - .isLessThan(ids.indexOf(lowLikes.getId())) - ); + assertAll( + () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK), + () -> assertThat(ids).containsExactly( + highLikes.getId(), + middleLikes.getId(), + lowLikes.getId() + ) + );As per coding guidelines:
**/*Test*.java에서는 플래키 가능성과 검증 강도를 점검해야 한다.Also applies to: 325-334
🤖 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/ProductV1ApiE2ETest.java` around lines 286 - 295, The current test in ProductV1ApiE2ETest uses indexOf-based checks on ids (derived from response.getBody().data().content().stream().map(ProductV1Dto.ProductResponse::id)) which can pass when items are missing; change the assertion to validate both presence and exact order by asserting the returned ID list exactly equals the expected sequence of cheapProduct.getId(), middleProduct.getId(), expensiveProduct.getId() (and add separate exact-order assertions for PRICE_ASC and LIKES_DESC cases referenced later at lines ~325-334). Replace the indexOf chain with a deterministic containsExactly/equals-style assertion against the ids list to ensure no items are omitted and order is correct.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/dto/ProductV1Dto.java-5-7 (1)
5-7:⚠️ Potential issue | 🟠 MajorDTO의 Bean Validation 어노테이션 사용을 제거해야 한다.
이 프로젝트는 검증을 도메인 레이어에서 수행하고
CoreException을 통해ApiControllerAdvice로 통일하는 아키텍처를 따르고 있다. 현재ProductV1Dto의@NotNull,@NotBlank어노테이션은 프로젝트 정책을 위반하며, Bean Validation 예외(MethodArgumentNotValidException)가 발생하면 도메인 레이어의CoreException흐름을 벗어나 응답 포맷과 에러 코드가 분산될 수 있다.Product.create()와Product.update()에서 이미 검증이 구현되어 있으므로, DTO의 어노테이션을 제거하면 검증 로직은 유지되고 오류 처리가 통일된다. E2E 테스트에서 잘못된 요청(null 값, 빈 문자열)이 항상 동일한 에러 포맷과 코드를 반환하는지 검증해야 한다.수정 예시 diff
-import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; @@ public record CreateRequest( - `@NotNull` Long brandId, - `@NotBlank`(message = "상품 이름은 필수값입니다.") String name, + Long brandId, + String name, String description, - `@NotNull` Integer price, - `@NotNull` Integer stockQuantity + Integer price, + Integer stockQuantity ) {} @@ public record UpdateRequest( - `@NotBlank`(message = "상품 이름은 필수값입니다.") String name, + String name, String description, - `@NotNull` Integer price, - `@NotNull` Integer stockQuantity, + Integer price, + Integer stockQuantity, Product.Visibility visibility ) {}🤖 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/product/dto/ProductV1Dto.java` around lines 5 - 7, ProductV1Dto에 선언된 Bean Validation 어노테이션(`@NotNull`, `@NotBlank`)을 제거하여 DTO에서 검증을 하지 않도록 변경하세요; 검증은 이미 도메인에서 처리되고 있으므로 ProductV1Dto의 `@NotNull/`@NotBlank 애노테이션을 삭제하고 관련 import(jakarta.validation.*)도 정리하여 MethodArgumentNotValidException가 발생하지 않게 하며, 기존 도메인의 검증 흐름(Product.create(), Product.update())과 ApiControllerAdvice/ CoreException 기반의 일관된 에러 처리가 유지되도록 변경 및 E2E 테스트에서 null/빈값 요청이 동일한 에러 포맷과 코드로 응답하는지 검증하세요.apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java-10-10 (1)
10-10:⚠️ Potential issue | 🟠 Major주문 조회가 무제한 리스트 반환이라 대량 기간 조회에서 장애 위험이 크다.
Line 10은 기간이 넓어질수록 한 번에 전량 적재되어 응답 지연과 메모리 급증을 유발할 수 있다. 운영 관점에서 타임아웃/GC 스파이크로 이어질 수 있으니,
Pageable기반 조회와 고정 정렬(createdAt,id)로 바꾸는 것이 안전하다. 추가로(user_id, created_at)인덱스 유무를 확인해야 한다.
추가 테스트로 사용자 주문 1,000건 이상 데이터에서 페이지 단위 조회가 크기 제한을 지키고 정렬이 안정적으로 유지되는지 통합 테스트를 보강해야 한다.수정 예시
public interface OrderJpaRepository extends JpaRepository<Order, Long> { - List<Order> findByUserIdAndCreatedAtBetween(Long userId, ZonedDateTime startAt, ZonedDateTime endAt); + Page<Order> findByUserIdAndCreatedAtBetween( + Long userId, + ZonedDateTime startAt, + ZonedDateTime endAt, + Pageable pageable + ); }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/order/OrderJpaRepository.java` at line 10, Replace the unbounded List-returning query in OrderJpaRepository (method findByUserIdAndCreatedAtBetween(Long userId, ZonedDateTime startAt, ZonedDateTime endAt)) with a Pageable-returning signature (e.g., Page<Order> findByUserIdAndCreatedAtBetween(Long userId, ZonedDateTime startAt, ZonedDateTime endAt, Pageable pageable)) and require callers to pass a Pageable that enforces a fixed sort (createdAt asc/desc, then id) to guarantee stable ordering; also verify or add a composite index on (user_id, created_at) in the schema/migration and add an integration test that inserts >1,000 orders for a user and asserts paged retrieval respects page size limits and stable sort across pages.apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java-20-21 (1)
20-21:⚠️ Potential issue | 🟠 Major대량 브랜드 삭제에서
productIds선적재 방식은 성능·안정성 리스크가 크다.Line 20~Line 21은 브랜드 상품 수가 많을 때 애플리케이션 메모리 사용량과 DB
IN파라미터 한계 문제를 유발해 배치성 장애로 이어질 수 있다. 운영 관점에서는 삭제 조건을 DB로 내려brandId기준 벌크 삭제로 바꾸는 것이 안전하다.
수정안으로LikeService.deleteAllByBrandId(brandId)경로를 추가해 저장소에서 조인/서브쿼리로 삭제하고, 파사드에서는 ID 리스트 적재를 제거하는 구조를 권장한다.
추가 테스트로 상품/좋아요가 많은 브랜드(예: 10,000건+) 삭제 시 시간/메모리 급증 없이 likes→products→brand 정합성이 유지되는 통합 테스트를 보강해야 한다.수정 방향 예시
`@Transactional` public void delete(Long brandId) { - List<Long> productIds = productService.getProductIdsByBrandId(brandId); - likeService.deleteAllByProductIds(productIds); + likeService.deleteAllByBrandId(brandId); productService.deleteAllByBrandId(brandId); brandService.delete(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/application/brand/BrandFacade.java` around lines 20 - 21, 현재 BrandFacade에서 productService.getProductIdsByBrandId(brandId)로 모든 상품 ID를 메모리에 올린 뒤 likeService.deleteAllByProductIds(productIds)를 호출하는 방식은 대량 데이터에서 메모리·DB IN 한계 문제를 유발하므로, productId 리스트 적재를 제거하고 LikeService에 deleteAllByBrandId(brandId) 메서드를 추가하여 저장소 레벨에서 brandId 기준으로 bulk delete(조인 또는 서브쿼리 사용)하도록 변경하세요; BrandFacade는 productService.getProductIdsByBrandId와 likeService.deleteAllByProductIds 호출을 제거하고 likeService.deleteAllByBrandId(brandId)만 호출하도록 수정하며, LikeRepository/DAO 쪽에 brandId 기반 삭제 쿼리를 구현하고 10,000건 이상 브랜드 삭제 시 메모리·시간 영향을 검증하는 통합 테스트(좋아요→상품→브랜드 정합성)를 추가하세요.apps/commerce-api/src/main/java/com/loopers/interfaces/config/WebConfig.java-23-35 (1)
23-35:⚠️ Potential issue | 🟠 Major인터셉터 제외 경로와
@LoginUser해석이 분리되어 인증 누락이 500으로 변질될 위험이 있다.현재 구조에서는 인터셉터가 실행되지 않은 요청에서도
LoginUserArgumentResolver가 nulluserId를 반환할 수 있어, 인증 실패가 일관된 인증 에러가 아니라 NPE성 500으로 번질 수 있다. 운영 관점에서 보안 이벤트 분류와 장애 분석이 어려워지는 패턴이다.LoginUserArgumentResolver에서userId부재 시CoreException으로 즉시 실패시키고, 인증 필요 경로를 include 기준으로 더 명확히 제한하는 구성이 안전하다. 추가로 “인터셉터 제외 경로 +@LoginUser” 요청이 일관된 에러 포맷으로 반환되는 MVC/E2E 테스트를 추가해야 한다.Based on learnings: In the loop-pack-be-l2-vol3-java project, 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/interfaces/config/WebConfig.java` around lines 23 - 35, Change authentication flow to fail fast and unify errors: update LoginUserArgumentResolver so when it cannot resolve a userId it throws CoreException (so errors route through ApiControllerAdvice) instead of returning null; tighten WebConfig interceptor configuration to use explicit includePathPatterns for authenticated endpoints (rather than relying only on excludePathPatterns) so only intended routes invoke the interceptor and resolver; add MVC/E2E tests asserting that requests to endpoints annotated with `@LoginUser` on excluded paths return the standardized CoreException/API error response (not a 500 NPE) to verify unified handling.apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemSnapshot.java-3-11 (1)
3-11:⚠️ Potential issue | 🟠 Major도메인 스냅샷 불변식 부재로 음수 금액/수량 및 오버플로우가 그대로 통과된다.
주문 금액 계산의 입력 경계가 이 값 객체에서 강제되지 않으면, 상위 계층 검증 누락 시 잘못된 총액이 생성되어 정산 오류로 이어질 수 있다.
productId/productName/unitPrice/quantity를 생성 시점에 검증하고, 금액 계산은Math.multiplyExact로 오버플로우를 차단해야 한다. 또한 예외 래핑 시 cause를 보존해 운영 로그에서 근본 원인을 추적 가능하게 해야 한다. 추가로quantity <= 0,unitPrice < 0, 곱셈 오버플로우 케이스 단위 테스트를 반드시 추가해야 한다.수정 예시
package com.loopers.domain.order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + public record OrderItemSnapshot( Long productId, String productName, long unitPrice, int quantity ) { + public OrderItemSnapshot { + if (productId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 ID는 필수다."); + } + if (productName == null || productName.isBlank()) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 필수다."); + } + if (unitPrice < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품 금액은 0 이상이어야 한다."); + } + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "수량은 1 이상이어야 한다."); + } + } + public long lineAmount() { - return (long) unitPrice * quantity; + try { + return Math.multiplyExact(unitPrice, quantity); + } catch (ArithmeticException e) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 금액 계산 중 오버플로우가 발생했다.", e); + } } }As per coding guidelines:
**/domain/**/*.java엔티티/값 객체 경계를 명확히 하고 불변성과 캡슐화를 점검하며,**/*.java예외 처리 시 cause를 보존한다.🤖 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/OrderItemSnapshot.java` around lines 3 - 11, The record OrderItemSnapshot currently allows negative/zero and overflow-prone values; add a compact constructor for OrderItemSnapshot to validate productId != null, productName != null/blank, quantity > 0 and unitPrice >= 0 and throw IllegalArgumentException on violations; update lineAmount() to use Math.multiplyExact(unitPrice, (long)quantity) inside a try/catch that wraps ArithmeticException in a new ArithmeticException (or a domain-specific exception) while preserving the caught exception as the cause; add unit tests covering quantity <= 0, unitPrice < 0, and multiplication overflow to assert exceptions are thrown.apps/commerce-api/src/main/java/com/loopers/application/user/UserService.java-42-56 (1)
42-56:⚠️ Potential issue | 🟠 Major인증/조회 실패 메시지에 식별자 노출이 있다.
운영 관점에서 로그인 ID와 사용자 ID가 예외 메시지로 노출되면 계정 열거와 개인정보 노출 리스크가 커지고, 외부 클라이언트에 내부 식별 규칙이 드러난다. 수정안으로 사용자 응답 메시지는 일반화하고, 상세 식별자는 내부 로그(마스킹)로만 분리해야 한다. 또한 인증 실패(없는 ID/잘못된 비밀번호)는 동일한 메시지로 통일해야 한다. 추가 테스트로 로그인 ID 미존재/비밀번호 불일치/사용자 ID 미존재 케이스에서 응답 메시지에 원본 식별자가 포함되지 않음을 검증해야 한다.
As per coding guidelines `**/*.java`: 예외 처리 시 cause를 보존하고, 사용자 메시지와 로그 메시지를 분리하도록 제안한다. 로깅 시 민감정보 노출 가능성을 점검한다.수정 예시
@@ `@Transactional`(readOnly = true) public User authenticate(String loginId, String loginPw) { - User user = getUserByLoginId(loginId); + User user = userRepository.findByLoginId(loginId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자 정보가 올바르지 않습니다.")); if (!passwordEncoder.matches(loginPw, user.getPassword())) { throw new CoreException(ErrorType.NOT_FOUND, "사용자 정보가 올바르지 않습니다."); } @@ private User getUserById(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, - "[userId = " + userId + "] 를 찾을 수 없습니다.")); + "사용자를 찾을 수 없습니다.")); }Also applies to: 58-61
🤖 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/user/UserService.java` around lines 42 - 56, Both authenticate(...) and getUserByLoginId(...) expose raw login identifiers in thrown CoreException messages; change them to return a single generic authentication/lookup failure message to clients (e.g. "사용자 정보가 올바르지 않습니다.") while preserving the original cause when constructing CoreException, and move detailed info (including masked loginId) into an internal log call (not the exception message). Specifically, update getUserByLoginId to throw CoreException with a generic message and include the underlying NoSuchElementException as the cause, update authenticate to throw the same generic message on password mismatch (again preserving cause), add logging statements that record the masked loginId for debugging, and add tests covering missing-loginId, wrong-password, and missing-user to assert no raw identifiers appear in exception messages.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java-54-77 (1)
54-77:⚠️ Potential issue | 🟠 Major날짜 파싱 실패와 역전된 기간 입력이 500 에러로 전파되므로 400 BAD_REQUEST로 즉시 차단해야 한다
Line 74-77에서 파싱 예외를 처리하지 않으면 잘못된
startAt/endAt입력이 500 INTERNAL_ERROR로 전파되어 운영 알람 노이즈와 불필요한 재시도를 유발한다. 또한startAt > endAt도 사전에 검증되지 않는다. parseZonedDateTime 메서드를 try-catch로 감싸서 DateTimeParseException을 CoreException(ErrorType.BAD_REQUEST)로 변환하고, getOrders 메서드에서parsedStartAt.isAfter(parsedEndAt)검증을 추가해 즉시 400으로 반환해야 한다. 추가로 "잘못된 날짜 형식"과 "역전된 기간" 각각에 대해 400 상태 코드와 표준 에러 바디를 검증하는 API 테스트를 OrderV1ApiE2ETest에 추가한다.🔧 제안 수정안
+import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import java.time.format.DateTimeParseException; ... public ApiResponse<List<OrderV1Dto.OrderResponse>> getOrders( `@LoginUser` Long userId, `@RequestParam` String startAt, `@RequestParam` String endAt ) { ZonedDateTime parsedStartAt = parseZonedDateTime(startAt); ZonedDateTime parsedEndAt = parseZonedDateTime(endAt); + if (parsedStartAt.isAfter(parsedEndAt)) { + throw new CoreException(ErrorType.BAD_REQUEST, "조회 시작일은 종료일보다 늦을 수 없습니다."); + } List<OrderV1Dto.OrderResponse> orders = orderService.getOrders(userId, parsedStartAt, parsedEndAt) .stream() .map(OrderV1Dto.OrderResponse::from) .toList(); return ApiResponse.success(orders); } private ZonedDateTime parseZonedDateTime(String value) { - String normalized = value.replace(" ", "+"); - return ZonedDateTime.parse(normalized, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + try { + String normalized = value.replace(" ", "+"); + return ZonedDateTime.parse(normalized, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } catch (DateTimeParseException e) { + throw new CoreException(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/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java` around lines 54 - 77, The parseZonedDateTime method currently throws DateTimeParseException which bubbles up as 500 and there is no check for inverted ranges; wrap the parsing in a try-catch inside parseZonedDateTime to catch DateTimeParseException and throw new CoreException(ErrorType.BAD_REQUEST, "잘못된 날짜 형식") (or similar standardized message), and in the controller method that calls orderService.getOrders (the getOrders endpoint) validate parsedStartAt.isAfter(parsedEndAt) and throw CoreException(ErrorType.BAD_REQUEST, "역전된 기간") when true so the request returns 400; also add API tests in OrderV1ApiE2ETest covering invalid date format and startAt > endAt to assert 400 error bodies.apps/commerce-api/src/main/java/com/loopers/application/like/LikeService.java-20-25 (1)
20-25:⚠️ Potential issue | 🟠 MajorTOCTOU 경쟁 조건으로 중복 좋아요가 발생할 수 있으며, 현재 제안된 예외 처리 방식은 CoreException API와 맞지 않다.
라인 20-25의 선조회 후 저장 패턴은 동시 요청에서 중복 방지 불가능하다. DB의
(user_id, product_id)유니크 제약이 최종 안전장치로 작동하지만, 제약 위반 시 발생하는 예외를 명확히 처리해야 한다.다만 현재
CoreException은 cause 파라미터를 지원하지 않으므로, 제안된 수정안은 컴파일되지 않는다. 다음 중 하나를 선택해야 한다:
CoreException을 확장하여 cause 체이닝 지원 추가- DB 제약 위반 시
DataIntegrityViolationException을 직접 처리하지 않고,register()호출자가 예외를 처리하도록 설계추가로 동시성 환경에서 "성공 1건, 실패 1건, 최종 row=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/application/like/LikeService.java` around lines 20 - 25, The current pre-check using likeRepository.findByUserIdAndProductId(...) causes TOCTOU races; remove the optimistic exists-check and instead attempt to persist the new Like (use Like.create(...) then likeRepository.save(...)) and handle DB uniqueness violations: wrap the save in a try/catch that catches DataIntegrityViolationException and translate it into the appropriate domain error. Since CoreException currently lacks a cause parameter, either (A) add a CoreException constructor accepting (ErrorType, String, Throwable) and throw new CoreException(ErrorType.ALREADY_LIKED, "이미 좋아요한 상품입니다.", ex) in the catch, or (B) do not convert the exception and let the caller of register() handle DataIntegrityViolationException per your chosen design; also add an integration test exercising concurrent requests to LikeService.register(...) to assert one success and one handled failure with final row count = 1.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java-39-39 (1)
39-39:⚠️ Potential issue | 🟠 Major
parseZonedDateTime()메서드에서 날짜 형식 오류가 발생하면 500 에러를 반환한다74~76줄의
parseZonedDateTime()메서드에서ZonedDateTime.parse()를 호출할 때 잘못된 날짜 형식이 입력되면DateTimeParseException이 발생한다. 이 예외는ApiControllerAdvice에서 처리되지 않아 500 에러(INTERNAL_ERROR)로 응답되므로, 클라이언트는 잘못된 요청임을 알 수 없다. 날짜 파싱 오류를CoreException으로 변환하여 400 에러를 반환하도록 수정해야 한다. 또한startAt과endAt파라미터의 대소 관계를 검증하는 로직도 추가하여 명확한 에러 메시지를 제공해야 한다.🤖 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/OrderV1Controller.java` at line 39, The parseZonedDateTime() call currently lets ZonedDateTime.parse(...) throw DateTimeParseException which bubbles up and yields a 500; update parseZonedDateTime() in OrderV1Controller to catch DateTimeParseException and throw a CoreException mapped to BAD_REQUEST with a clear message (e.g., "invalid date format for startAt/endAt: <input>"); after parsing both startAt and endAt in the create flow validate their ordering and, if startAt is after endAt, throw a CoreException BAD_REQUEST with a descriptive message (e.g., "startAt must be before or equal to endAt"); ensure these CoreException throws are used so ApiControllerAdvice returns 400 to the client.apps/commerce-api/src/main/java/com/loopers/interfaces/api/user/UserV1Controller.java-33-37 (1)
33-37:⚠️ Potential issue | 🟠 Major@Valid을 제거하고 검증 흐름을 도메인 계층으로 일원화해야 한다
35번 줄의
@Valid어노테이션은 프로젝트의 헥사고날 아키텍처 원칙을 위반한다. 현재 빈 검증(Bean Validation)과 도메인 검증(PasswordPolicyValidator)이 분산되어 있어, 새 비밀번호 검증이 일관성 없이 처리된다. 비밀번호 필수값 확인은@NotBlank로 처리하고, 비밀번호 정책(8~16자, 생년월일 미포함)은 PasswordPolicyValidator에서 처리하도록 분리되는 것이 운영 중 예측 불가능한 에러를 야기한다.@Valid를 제거하면 모든 검증이 도메인 계층에서 CoreException으로 일원화되어 ApiControllerAdvice를 통해 표준 에러 응답으로 반환된다.또한 현재 E2E 테스트는 유효한 요청, 사용자 미존재, 현재 비밀번호 오류, 중복 변경만 검증하고 있다. 새 비밀번호가 비어있거나 정책을 위반하는 경우(예: 8자 미만, 특수문자 미포함, 생년월일 포함)에 대한 API 응답 포맷 테스트가 부재하다. 검증 오류 시나리오별로 표준 ApiResponse 형식이 유지되는지 확인하는 테스트를 추가해야 한다.
🔧 제안 수정안
public ApiResponse<Void> updatePassword( `@LoginUser` Long userId, `@RequestHeader`("X-Loopers-LoginPw") String currentPassword, - `@Valid` `@RequestBody` UserV1Dto.UpdatePasswordRequest request + `@RequestBody` UserV1Dto.UpdatePasswordRequest request ) { userService.updatePassword(UpdatePasswordCommand.from(userId, currentPassword, request)); return ApiResponse.success(null); }그리고 UserV1Dto의 UpdatePasswordRequest에서
@NotBlank어노테이션도 제거하여, 모든 입력값 검증을 도메인 계층에 위임해야 한다.🤖 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 33 - 37, Remove the controller-level bean validation and centralize password validation in the domain: in UserV1Controller.remove the `@Valid` from the UpdatePasswordRequest parameter (method handling updatePassword) and in UserV1Dto.UpdatePasswordRequest remove any `@NotBlank`; ensure UpdatePasswordCommand.from still maps raw values and let PasswordPolicyValidator (domain) throw CoreException for empty/invalid new passwords; ensure ApiControllerAdvice maps those CoreException errors to the standard ApiResponse format. Also add E2E tests covering new-password-empty and policy-violation cases to assert the API returns the standardized ApiResponse for each validation failure.apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java-14-21 (1)
14-21:⚠️ Potential issue | 🟠 Major프로젝트 가이드라인에 따라 Bean Validation 어노테이션 사용을 지양해야 한다.
이 프로젝트에서는 DTO에
@NotNull,@NotEmpty,@Min등의 Bean Validation 어노테이션 대신 도메인 레이어(엔티티 생성자, 서비스)에서 검증하고CoreException을 통해 일관된 에러 응답을 반환하는 패턴을 사용한다.OrderService.validateItems에서 이미 검증 로직이 존재하므로 중복된다.🛠️ 수정안
public record CreateRequest( - `@NotNull` `@NotEmpty` List<OrderItemRequest> items + List<OrderItemRequest> items ) {} public record OrderItemRequest( - `@NotNull` Long productId, - `@NotNull` `@Min`(1) Integer quantity + Long productId, + Integer quantity ) {}검증은
OrderService.validateItems또는Order.create에서CoreException을 통해 수행되어야 한다. Based on learnings: "Do not use Bean Validation annotations on DTOs in this project. Move validation logic into the domain layer."🤖 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 - 21, Remove Bean Validation annotations from the DTO records CreateRequest and OrderItemRequest (drop `@NotNull`, `@NotEmpty`, `@Min`) and rely on the existing domain validation; ensure any validation of items is performed via OrderService.validateItems or Order.create and that failures throw CoreException to produce the project-standard error response; update usages/tests that assumed bean validation accordingly so DTOs are simple data carriers and all checks move to OrderService.validateItems / Order.create with CoreException on invalid input.apps/commerce-api/src/main/java/com/loopers/application/order/OrderService.java-69-75 (1)
69-75:⚠️ Potential issue | 🟠 MajorgetOrderItems 메서드에 소유권 검증이 없다. 서비스 계층에 authorization 책임을 추가하다.
getOrder(userId, orderId)메서드는order.getUserId().equals(userId)검증을 수행하지만,getOrderItems(orderId)메서드는 userId 파라미터가 없고 소유권 검증이 없다. 현재는 컨트롤러에서getOrder호출 이후에getOrderItems를 호출하므로 보호되지만, 서비스 계층의 자기 방어 원칙(defense in depth)에 위반된다. 향후 새로운 엔드포인트 추가나 리팩토링 시 접근 제어 우회 가능성이 있다.
getOrderItems(Long userId, Long orderId)형태로 변경하여getOrder와 일관성 있게 소유권 검증을 수행하고, 테스트에서 userId 불일치 시 NOT_FOUND 예외 발생을 확인한다.🤖 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/OrderService.java` around lines 69 - 75, Change getOrderItems to accept Long userId as the first parameter (signature getOrderItems(Long userId, Long orderId)), perform the same ownership check used by getOrder by loading the Order (reuse getOrder(userId, orderId) or fetch Order and verify order.getUserId().equals(userId)) and throw the same NOT_FOUND behavior when the user doesn't own the order, then return orderItemRepository.findByOrderId(orderId).stream().map(OrderItemInfo::from).toList(); keep `@Transactional`(readOnly = true) and update callers/tests to pass userId and assert NOT_FOUND on mismatch.apps/commerce-api/src/main/java/com/loopers/application/order/OrderService.java-49-55 (1)
49-55:⚠️ Potential issue | 🟠 MajorgetOrders 메서드의 날짜 범위 쿼리 경계 조건과 타임존 처리를 검증해야 한다.
findByUserIdAndCreatedAtBetween은 JPA의 BETWEEN으로 구현되며, 이는 SQL 표준에 따라 양 끝값을 포함한다(inclusive). 다만 다음 문제가 있다:
경계값 테스트 부재: 현재 E2E 테스트는 day-range 범위(±1일)만 검증하고, 주문이 정확히 startAt 또는 endAt 타임스탬프에 생성되는 경계 경우를 테스트하지 않는다. nanosecond 단위의 정확성이나 데이터베이스의 precision에 따라 누락/중복이 발생할 수 있다.
타임존 변환 미검증: createdAt은 ZonedDateTime으로 저장되지만, 데이터베이스 TIMESTAMP 컬럼의 timezone 처리와 JPA 변환 설정이 명확하지 않다. 운영 시 서로 다른 타임존의 사용자가 조회할 때 의도치 않은 데이터 누락이 발생할 수 있다.
매개변수 검증 부재: startAt/endAt의 null 체크 또는 순서 검증(startAt <= endAt)이 없어, 잘못된 범위 요청 시 동작이 불명확하다.
수정안: (1) 경계값 테스트 추가 - 주문 생성 시각과 정확히 일치하는 startAt/endAt으로 조회하는 test case, (2) application.properties에서 timezone 설정 명시 및 DB 설정 검증, (3) getOrders 메서드에 startAt/endAt 검증 로직 추가.
🤖 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/OrderService.java` around lines 49 - 55, getOrders currently calls orderRepository.findByUserIdAndCreatedAtBetween which is inclusive and lacks parameter validation and timezone/precision handling; update getOrders to validate startAt and endAt (non-null and startAt <= endAt), normalize/convert both ZonedDateTime parameters to the DB canonical timezone (or UTC) before querying (use the same conversion used for persistence), and avoid precision loss by adjusting endAt to the DB precision if necessary (or use an explicit >=/<= repository query instead of BETWEEN); also add E2E unit tests for boundary cases where createdAt == startAt and createdAt == endAt and verify application.properties DB timezone (and JPA mapping) is explicitly set to the chosen timezone so timezone conversions are deterministic.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java-11-11 (1)
11-11:⚠️ Potential issue | 🟠 MajorBean Validation 어노테이션을 DTO에서 제거하고 검증을 도메인 레이어로 이동하다
Line 41, 74의
@Valid및 ProductV1Dto의@NotNull,@NotBlank어노테이션은 프로젝트 아키텍처 원칙에 위배된다. 이 프로젝트는 모든 검증을 도메인 레이어에서 수행하고CoreException으로만 실패를 반환하도록 설계되었다. 현재 Bean Validation 사용으로 인해MethodArgumentNotValidException경로가 별도로 생기면서 에러 처리 일관성이 유지되지 않는다.수정안:
- ProductV1Dto에서
@NotNull,@NotBlank어노테이션 제거- 컨트롤러의
@Valid제거- ProductCreateCommand, ProductUpdateCommand 생성 단계에서 도메인 검증 수행 후 검증 실패 시
CoreException발생하도록 통일- 마찬가지로 ProductFacade와 ProductService의 명령 실행 시점에서도 도메인 검증 강화
테스트 추가:
- 잘못된
name(공백),price(음수),stockQuantity(음수) 등으로 POST/PUT 호출 시 항상 동일한 에러 스키마(BAD_REQUEST)와 코드로 응답되는 E2E 테스트 작성🤖 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/product/AdminProductV1Controller.java` at line 11, Remove bean validation annotations from ProductV1Dto and strip the `@Valid` annotations from AdminProductV1Controller (lines where requests are accepted), then move all field validations into domain-level constructors/factories used by ProductCreateCommand and ProductUpdateCommand so that invalid inputs (blank name, negative price, negative stockQuantity) throw CoreException; also add defensive validation in ProductFacade and ProductService entry points to ensure any command execution re-validates and re-throws CoreException on failure. Update command creation paths to translate former DTO constraint checks into explicit domain checks and error messages wrapped in CoreException, and remove any handling for MethodArgumentNotValidException so error handling is consistent. Finally add E2E tests that POST/PUT with invalid name/price/stock produce the same BAD_REQUEST error schema and code for all validation failures.apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java-95-110 (1)
95-110:⚠️ Potential issue | 🟠 Major좋아요 카운트 증감의 동시성 안전성을 원자적 UPDATE로 보장해야 한다
증감 메서드는 엔티티 조회 후 메모리에서 값을 수정하고 dirty checking으로 UPDATE를 실행하는 구조이므로, 서로 다른 트랜잭션에서 동시에 요청될 때 lost update가 발생한다. 예를 들어 두 요청이 동시에 같은 상품의 좋아요 카운트를 읽으면 각각의 증감 결과 중 하나만 반영되어 카운트가 정확하지 않게 된다. 운영 환경에서는 정렬/랭킹의 품질 저하로 바로 노출되는 문제다.
현재 codebase에서
stockQuantity는ProductJpaRepository의@Query원자 UPDATE로 동시성을 보호하고 있으므로, 동일한 패턴을likeCount에도 적용해야 한다. 아래 수정안 중 하나를 선택하여 적용한다.
ProductRepository에 원자적 UPDATE 메서드 추가:// ProductRepository 인터페이스 void increaseLikeCount(Long productId); void decreaseLikeCount(Long productId); // ProductJpaRepository `@Modifying`(clearAutomatically = true) `@Query`(""" update Product p set p.likeCount = p.likeCount + 1 where p.id = :productId and p.deletedAt is null and p.visibility = :visibility """) void increaseLikeCount(`@Param`("productId") Long productId, `@Param`("visibility") Product.Visibility visibility); `@Modifying`(clearAutomatically = true) `@Query`(""" update Product p set p.likeCount = case when p.likeCount > 0 then p.likeCount - 1 else 0 end where p.id = :productId and p.deletedAt is null and p.visibility = :visibility """) void decreaseLikeCount(`@Param`("productId") Long productId, `@Param`("visibility") Product.Visibility visibility);
- 또는
@Version을 Product 엔티티에 추가하고 재시도 로직 구현.동시성 통합 테스트를 추가하여 500~1000건 동시 요청 시 기대 카운트와 최종 카운트가 일치함을 검증한다.
🤖 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/product/ProductService.java` around lines 95 - 110, The likeCount increment/decrement is vulnerable to lost updates because ProductService.increaseLikeCount and decreaseLikeCount load the entity and mutate it in-memory; replace this with an atomic DB UPDATE: add methods increaseLikeCount(Long productId, Product.Visibility visibility) and decreaseLikeCount(Long productId, Product.Visibility visibility) to the ProductRepository and implement them in ProductJpaRepository as `@Modifying` `@Query` updates (increase by 1; decrease by 1 with a floor-at-zero case) mirroring the stockQuantity pattern, then change ProductService.increaseLikeCount and decreaseLikeCount to call these repository methods inside a `@Transactional` context (removing the entity in-memory mutation) and add a concurrency integration test that fires hundreds of parallel requests and asserts final likeCount equals expected.
| public void decreaseStock(List<OrderItemCommand> items) { | ||
| List<OrderItemCommand> sorted = items.stream() | ||
| .sorted(Comparator.comparing(OrderItemCommand::productId)) | ||
| .toList(); | ||
|
|
||
| for (OrderItemCommand item : sorted) { | ||
| boolean decreased = productRepository.decreaseStockIfEnough(item.productId(), item.quantity()); | ||
| if (!decreased) { | ||
| throw new CoreException(ErrorType.INSUFFICIENT_STOCK, "재고가 부족한 상품이 있습니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
재고 차감 입력 경계 검증 누락으로 재고 데이터가 오염될 수 있다
- 문제: Line 114-124에서
quantity가 0/음수/null이거나productId가 null인 항목을 선제 차단하지 않아, 잘못된 요청이 들어오면 재고 증가/500 오류/데이터 불일치로 이어질 수 있다. 운영에서는 재고 정합성 붕괴가 직접 장애로 연결된다. - 수정안: 루프 진입 전에
items와 각 항목의productId,quantity(>=1)를 검증하고 실패 시CoreException으로 즉시 중단해야 한다. - 추가 테스트:
quantity=0,quantity<0,productId=null,items에 null 포함 케이스에서 예외가 발생하고 재고가 전혀 변경되지 않는 테스트를 추가해야 한다.
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/product/ProductService.java`
around lines 114 - 124, Validate the input list and each OrderItemCommand before
mutating stock: in ProductService.decreaseStock first check that items and no
element are null, then iterate (or use stream) to ensure every
OrderItemCommand.productId() != null and OrderItemCommand.quantity() >= 1; if
any check fails throw new CoreException(ErrorType.INSUFFICIENT_STOCK, "...") (or
appropriate ErrorType/message) before calling
productRepository.decreaseStockIfEnough so no partial updates occur; update or
add tests for ProductService.decreaseStock covering items == null, items
containing null, productId == null, quantity == 0 and quantity < 0 to assert a
CoreException is thrown and that productRepository.decreaseStockIfEnough is
never invoked.
| @Query(""" | ||
| update Product p | ||
| set p.stockQuantity = p.stockQuantity - :quantity | ||
| where p.id = :productId | ||
| and p.deletedAt is null | ||
| and p.visibility = :visibility | ||
| and p.stockQuantity >= :quantity | ||
| """) | ||
| int decreaseStockIfEnough( | ||
| @Param("productId") Long productId, | ||
| @Param("quantity") Integer quantity, | ||
| @Param("visibility") Product.Visibility visibility | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -path "*commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 179
🏁 Script executed:
fd "ProductJpaRepository" --type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 177
🏁 Script executed:
rg "decreaseStockIfEnough" --type java -A 10 -B 5Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 7730
🏁 Script executed:
cat -n apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1374
🏁 Script executed:
rg "OrderItemCommand" --type java -A 5 -B 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 13772
🏁 Script executed:
rg "decreaseStockIfEnough" --type java --context 15 apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1366
🏁 Script executed:
fd "ProductServiceTest" --type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 172
🏁 Script executed:
rg "OrderItemCommand|quantity" apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/ --type java -B 3 -A 3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4559
🏁 Script executed:
fd "OrderV1Dto" --type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 165
🏁 Script executed:
cat -n apps/commerce-api/src/test/java/com/loopers/application/product/ProductServiceTest.java | head -60Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2513
🏁 Script executed:
cat -n apps/commerce-api/src/test/java/com/loopers/application/product/ProductServiceTest.java | tail -80Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3278
🏁 Script executed:
rg "decreaseStock" --type java -B 5 -A 10 apps/commerce-api/src/test/java/Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 10981
재고 차감 쿼리에 수량 양수 조건이 없어 데이터 무결성 위험이 있다.
현재 컨트롤러 레이어의 @Min(1) 검증과 도메인 모델의 유효성 검사가 0 이상의 수량을 걸러내고 있으나, 저장소 쿼리 레이어에는 수량 양수 조건이 없다. 캐싱 우회, 테스트 픽스처 직접 호출, 또는 상위 계층 검증 누락 상황에서 음수 수량이 쿼리에 도달하면 stockQuantity - (-n)으로 재고가 증가하여 정합성이 손상될 수 있다. 방어적 프로그래밍 원칙에 따라 쿼리 WHERE 절에 :quantity > 0 조건을 추가해야 한다.
수정 예시 diff
`@Query`("""
update Product p
set p.stockQuantity = p.stockQuantity - :quantity
where p.id = :productId
+ and :quantity > 0
and p.deletedAt is null
and p.visibility = :visibility
and p.stockQuantity >= :quantity
""")또한 저장소 통합 테스트에서 quantity=0, quantity=-1일 때 업데이트 건수가 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/infrastructure/product/ProductJpaRepository.java`
around lines 16 - 28, The update query in decreaseStockIfEnough allows negative
quantities which can increase stock; modify the JPQL WHERE clause in the
decreaseStockIfEnough method to include a defensive check that :quantity > 0 so
updates only proceed for positive amounts, and ensure the `@Param` signature
remains unchanged (productId, quantity, visibility). Also add repository
integration tests that call decreaseStockIfEnough with quantity=0 and
quantity=-1 and assert the returned update count is 0 and the
Product.stockQuantity remains unchanged.
| @PostMapping | ||
| @ResponseStatus(HttpStatus.CREATED) | ||
| public ApiResponse<ProductV1Dto.AdminProductResponse> createProduct( | ||
| @RequestHeader("X-Loopers-Ldap") String ldap, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 먼저 AdminProductV1Controller 파일의 실제 내용 확인
echo "=== AdminProductV1Controller.java 파일 확인 ==="
find apps/commerce-api -name "AdminProductV1Controller.java" -type f 2>/dev/null | head -5
# 파일이 있으면 내용 확인
if [ -f "apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java" ]; then
wc -l "apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java"
echo "=== 처음 100줄 ==="
head -100 "apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java"
fiRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4343
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 인증 인터셉터/리졸버 등록 확인
echo "=== 인증 인터셉터/리졸버/필터 설정 파일 검색 ==="
rg -n --type=java -l 'addInterceptors|addArgumentResolvers|InterceptorRegistry|ArgumentResolverRegistry|FilterRegistrationBean|SecurityConfig|WebConfig|LoginUserInterceptor' \
apps/commerce-api/src/main/java --max-count=20
# admin 컨트롤러의 헤더 의존 패턴 확인
echo "=== admin 엔드포인트 헤더 검증 패턴 ==="
rg -n --type=java '@RequestHeader.*X-Loopers-Ldap' \
apps/commerce-api/src/main/java/com/loopers/interfaces/api -C 3
# /api-admin 경로 검색
echo "=== /api-admin 경로 설정 ==="
rg -n --type=java '/api-admin' \
apps/commerce-api/src/main/java --max-count=30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 12328
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Spring Security 설정 또는 인터셉터 설정 파일 찾기
echo "=== Spring Security/WebConfig 파일 검색 ==="
find apps/commerce-api/src/main/java -type f -name "*Config.java" -o -name "*Security*.java" | head -20
# WebMvcConfigurer 구현 클래스 찾기
echo "=== WebMvcConfigurer 구현 확인 ==="
rg -n --type=java 'WebMvcConfigurer|addInterceptors|addArgumentResolvers' \
apps/commerce-api/src/main/java --max-count=50Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 829
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# WebConfig.java 전체 내용 확인
echo "=== WebConfig.java 전체 내용 ==="
cat -n "apps/commerce-api/src/main/java/com/loopers/interfaces/config/WebConfig.java"
echo ""
echo "=== LoginUserInterceptor.java 전체 내용 ==="
cat -n "apps/commerce-api/src/main/java/com/loopers/interfaces/api/auth/LoginUserInterceptor.java"Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 3202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Spring Security 설정이 있는지 확인
echo "=== Spring Security 관련 파일 검색 ==="
find apps/commerce-api/src/main/java -type f \( -name "*Security*.java" -o -name "*Filter*.java" \) | head -20
# admin 경로에 대한 보안 설정 확인
echo "=== WebConfig에서 admin 경로 관련 설정 확인 ==="
rg -n --type=java 'api-admin' apps/commerce-api/src/main/java/com/loopers/interfaces/config/
# admin 엔드포인트 테스트 코드 확인
echo "=== Admin Controller 테스트 파일 검색 ==="
find apps/commerce-api/src -name "*AdminProductV1ControllerTest*" -o -name "*AdminProductControllerTest*" 2>/dev/null
# 테스트에서 헤더 검증 여부 확인
echo "=== 테스트 파일 목록 ==="
find apps/commerce-api/src -name "*Test.java" -type f | grep -i admin | head -10Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# application.properties 또는 yml에서 보안 설정 확인
echo "=== 설정 파일에서 admin 경로 보호 확인 ==="
find apps/commerce-api -name "application*.yml" -o -name "application*.properties" | xargs cat 2>/dev/null | head -100
# 헤더가 실제로 비즈니스 로직에서 사용되는지 확인
echo "=== ldap 파라미터 사용 여부 확인 ==="
rg -n 'ldap' apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java
# 다른 admin 컨트롤러도 동일한 문제가 있는지 확인
echo "=== 모든 admin 컨트롤러 목록 ==="
find apps/commerce-api/src/main/java -name "Admin*Controller.java" -type fRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1714
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# ldap 파라미터가 실제로 메서드 본문에서 사용되는지 더 자세히 확인
echo "=== AdminProductV1Controller 전체 내용에서 ldap 사용 확인 ==="
cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/AdminProductV1Controller.java | grep -A 20 "@RequestHeader"
# 다른 admin 컨트롤러도 동일한 패턴인지 확인
echo "=== AdminOrderV1Controller에서 ldap 사용 확인 ==="
rg -n -A 15 '@RequestHeader.*X-Loopers-Ldap' apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/AdminOrderV1Controller.java | head -40
echo "=== AdminBrandV1Controller에서 ldap 사용 확인 ==="
rg -n -A 15 '@RequestHeader.*X-Loopers-Ldap' apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/AdminBrandV1Controller.java | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 5528
관리자 인증이 중앙에서 강제되지 않아 신규 API 추가 시 보호 누락 위험이 있다
현재 /api-admin/** 경로는 WebConfig의 인터셉터에 등록되지 않았다. 각 Admin 컨트롤러에서 @RequestHeader("X-Loopers-Ldap") 헤더를 반복 선언하지만, LoginUserInterceptor는 이를 검증하지 않으며, 컨트롤러 메서드 본문에서도 ldap 파라미터를 사용하지 않는다. 이는 신규 관리자 API 추가 시 헤더 누락으로 무방비 노출될 수 있는 운영/보안 리스크를 초래한다.
/api-admin/** 경로를 WebConfig의 인터셉터에 명시적으로 등록하고 ldap 헤더 검증을 LoginUserInterceptor에 추가하거나, 별도의 관리자 전용 인터셉터를 작성하여 모든 admin 엔드포인트를 중앙에서 강제하도록 통합해야 한다. 이후 컨트롤러는 중복 헤더 선언을 제거하고 필요시 SecurityContext 또는 커스텀 ArgumentResolver를 통해 인증된 관리자 정보만 주입받도록 리팩터링하는 것이 안전하다.
모든 admin 라우트에 대해 무헤더 요청, 잘못된 헤더, 정상 헤더 케이스를 MVC 보안 테스트에서 매트릭스 형태로 검증하여 누락된 엔드포인트가 없는지 확인해야 한다.
🤖 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/product/AdminProductV1Controller.java`
at line 40, The admin endpoints rely on a duplicated
`@RequestHeader`("X-Loopers-Ldap") in controllers (e.g. AdminProductV1Controller)
instead of central enforcement, so register the /api-admin/** path in
WebConfig's interceptor registry and add LDAP header validation into
LoginUserInterceptor (or create a dedicated AdminInterceptor) to reject requests
with missing/invalid X-Loopers-Ldap; after centralizing validation, remove the
redundant `@RequestHeader` usage from admin controllers and switch controllers to
obtain authenticated admin info via SecurityContext or a custom
ArgumentResolver; add MVC tests covering no-header, invalid-header, and
valid-header cases across all /api-admin/** routes to ensure no admin endpoint
is unprotected.
📌 Summary
🧭 Context & Decision
문제 정의
선택지와 결정
1. Service 레이어 위치 —
domain/vsapplication/domain/에 배치 (DDD 멘토 권장 방향)application/에 배치BrandService,ProductService등 모두application/하위 유지2. BrandFacade / ProductFacade 도입 및 Controller 진입점 전략
BrandFacade(브랜드 연쇄 삭제 조율) +ProductFacade(브랜드 검증/좋아요 연쇄 삭제) 도입. 단, Controller에서 Facade를 단일 진입점으로 강제하지 않고 오케스트레이션 필요 여부로 구분ProductService → BrandService의존이 이미 존재하므로BrandService → ProductService를 추가하면 순환 의존 발생. Facade가 상위에서 이를 끊음. A(직접 참조)는 순환 위험, B(단일 진입점)는 오케스트레이션 없는 단순 위임 메서드가 Facade에 쌓여 비대해짐3. 헤더 기반 인증 — 인터셉터/ArgumentResolver 도입
X-Loopers-LoginId/LoginPw헤더를 받아UserService직접 호출LoginUserInterceptor+LoginUserArgumentResolver로 인증 분리@LoginUser Long userId어노테이션으로 Controller에서 인증 책임 제거UserService를 직접 호출하면 인증 로직이 산재되고 IDOR 취약점(loginId만으로 타인 정보 조회 가능) 발생. 인터셉터로 분리하면 secure-by-default — 새 API 추가 시 자동으로 인증 적용, 공개 엔드포인트만 명시적 제외excludePathPatterns누락 시 의도치 않게 인증이 걸릴 수 있음4. LikeCount — 카운터 컬럼 방식
Product.likeCount카운터 컬럼likeCount는likes테이블의 파생 데이터 — 동기화 누락 시 카운터 오차 발생 가능. 브랜드 삭제 연쇄 경로 등 예외 경로에서 누락되지 않도록 주의 필요5. 좋아요 취소 멱등화
6. 좋아요 목록 조회 URL —
/me패턴GET /api/v1/users/{userId}/likesGET /api/v1/users/me/likes/api/v1/users/me패턴과 일관성 유지7. 어드민 상품 목록 조회 — 삭제 상품 필터 위치
deletedAt IS NULL필터링deletedAt IS NULL)이 Repository 계층에 포함됨.ProductRepositoryImpl과InMemoryProductRepository양쪽에 동일 조건을 유지해야 함🏗️ Design Overview
변경 범위
영향 받는 모듈/도메인:
commerce-api/ Brand, Product, Like, Order, User(인증 개선)주요 신규 파일 (도메인별):
Brand
domain/brand/Brand.java,BrandRepository.javaapplication/brand/BrandService.java,BrandFacade.java,BrandInfo.javainfrastructure/brand/BrandRepositoryImpl.java,BrandJpaRepository.javainterfaces/api/brand/BrandV1Controller.java,AdminBrandV1Controller.java,BrandV1Dto.javaProduct
domain/product/Product.java(visibility enum, likeCount, active 네이밍 포함),ProductRepository.java,ProductOrder.javaapplication/product/ProductService.java,ProductFacade.java,ProductInfo.java,ProductSort.java,ProductCreateCommand.java,ProductUpdateCommand.javainfrastructure/product/ProductRepositoryImpl.java(QueryDSL,deletedAt IS NULL필터)interfaces/api/product/ProductV1Controller.java,AdminProductV1Controller.java,ProductV1Dto.javaLike
domain/like/Like.java(BaseEntity 미상속, hard delete),LikeRepository.javaapplication/like/LikeService.java,LikeFacade.java,LikeInfo.java,LikedProductInfo.javainfrastructure/like/LikeRepositoryImpl.java,LikeJpaRepository.javainterfaces/api/like/LikeV1Controller.java,LikeV1Dto.javaOrder
domain/order/Order.java,OrderItem.java,OrderRepository.java,OrderItemRepository.javaapplication/order/OrderService.java,OrderFacade.java,OrderInfo.java,OrderItemInfo.java,OrderCreateCommand.java,OrderItemCommand.javainfrastructure/order/OrderRepositoryImpl.java,OrderItemRepositoryImpl.javainterfaces/api/order/OrderV1Controller.java,AdminOrderV1Controller.java,OrderV1Dto.java인증
interfaces/api/auth/LoginUser.java,LoginUserInterceptor.java,LoginUserArgumentResolver.javainterfaces/config/WebConfig.java공통
interfaces/api/PageResponse.java—Page<T>역직렬화 문제 해결용 record DTO주요 수정 파일:
application/user/SignUpService.java— SignUpValidator 제거, 검증 로직 레이어 분리application/user/UserService.java—authenticate()추가,getMyInfo(Long userId)변경application/brand/BrandFacade.java— 좋아요 연쇄 삭제 추가application/product/ProductService.java— BrandService 의존 제거, likeCount 증감, 재고 차감infrastructure/product/ProductRepositoryImpl.java— 어드민 목록 조회deletedAt IS NULL필터 추가test/../InMemoryProductRepository.java— 테스트 더블 동일 정책 적용test/../AdminProductV1ApiE2ETest.java— 삭제 상품 미노출 E2E 케이스 추가주요 컴포넌트 책임
BrandFacade@Transactional)ProductFacadeLikeFacadeOrderFacadeLoginUserInterceptor/api/v1/**경로 전처리 — 헤더 검증 +userId주입ProductRepositoryImpldeletedAt IS NULL)🔁 Flow Diagram
브랜드 삭제 연쇄 흐름
sequenceDiagram autonumber actor Admin as 어드민 participant AdminBrandV1Controller participant BrandFacade participant ProductService participant LikeService participant BrandService Admin->>AdminBrandV1Controller: DELETE /api-admin/v1/brands/{brandId} AdminBrandV1Controller->>BrandFacade: delete(brandId) BrandFacade->>ProductService: getProductIdsByBrandId(brandId) ProductService-->>BrandFacade: productIds BrandFacade->>LikeService: deleteAllByProductIds(productIds) [hard delete] BrandFacade->>ProductService: deleteAllByBrandId(brandId) [soft delete] BrandFacade->>BrandService: delete(brandId) [soft delete] BrandFacade-->>AdminBrandV1Controller: 완료 AdminBrandV1Controller-->>Admin: 204 No Content주문 생성 흐름