Skip to content

[3주차] 도메인 주도 설계 구현 - 이주옥#106

Open
juoklee wants to merge 17 commits intoLoopers-dev-lab:juokleefrom
juoklee:round3
Open

[3주차] 도메인 주도 설계 구현 - 이주옥#106
juoklee wants to merge 17 commits intoLoopers-dev-lab:juokleefrom
juoklee:round3

Conversation

@juoklee
Copy link

@juoklee juoklee commented Feb 26, 2026

📌 Summary

  • 배경: 이커머스 핵심 흐름을 구현해야 한다.
  • 목표: Brand, Product, Address, Like, Order 5개 도메인을 레이어드 아키텍처 + DIP 원칙에 따라 구현하고, Admin API까지 포함한 백엔드를 로직을 완성한다.
  • 결과: Member를 포함한 6개 도메인, 30개 이상의 API 엔드포인트, 130개 파일 변경(+13,000줄), 단위/E2E 테스트 전체 통과.

🧭 Context & Decision

1. 좋아요 도메인 구조

  • 고려한 대안:
    • A: ProductLike / BrandLike 분리 (타겟별 별도 Entity)
    • B: 단일 Like Entity + targetType/targetId 다형 구조
  • 최종 결정: B (단일 다형 구조)
  • 트레이드오프: 타입 안전성은 다소 낮지만, 신규 타겟 추가 시 Entity 생성 없이 Enum 추가만으로 확장 가능. UK(member_id, target_type, target_id)로 무결성 보장.

2. 좋아요 삭제 전략

  • 고려한 대안:
    • A: Soft delete (BaseEntity 상속, deleted_at 사용)
    • B: Hard delete (물리 삭제)
  • 최종 결정: B (Hard delete)
  • 트레이드오프: 좋아요 이력을 보존하지 않지만, UK 충돌 없이 토글이 단순해지고, 불필요한 데이터가 쌓이지 않는다.

3. 주문 시 재고 차감 전략

  • 고려한 대안:
    • A: 주문 확정 시 차감 (후차감)
    • B: 주문 생성 시 선차감, All-or-Nothing
  • 최종 결정: B (선차감)
  • 트레이드오프: 주문 즉시 재고가 반영되어 과판매 방지. 하나라도 재고 부족 시 전체 주문 실패 + 트랜잭션 롤백으로 일관성 보장.

4. 주문-배송지/상품 관계

  • 고려한 대안:
    • A: FK로 Address/Product 직접 참조
    • B: 스냅샷 복사 (주문 시점 값을 Order/OrderItem에 저장)
  • 최종 결정: B (스냅샷)
  • 트레이드오프: 데이터 중복이 발생하지만, 배송지 삭제/상품 가격 변경 후에도 주문 이력이 당시 기준으로 보존된다.

5. Domain Reader의 Spring Data 의존

  • 고려한 대안:
    • A: Domain Reader가 Page/Pageable(Spring Data)을 직접 사용
    • B: Domain에 PageResult를 정의하고, Infrastructure에서 변환
  • 최종 결정: B (PageResult 도입)
  • 트레이드오프: 변환 코드가 추가되지만, Domain Layer가 Spring Data에 의존하지 않아 순수한 도메인 모델 유지.

6. Facade → Reader 직접 참조

  • 고려한 대안:
    • A: Facade가 Reader를 직접 호출 (빠르지만 계층 우회)
    • B: Facade → Service → Reader 경유 (계층 일관성)
  • 최종 결정: B (Service 경유)
  • 트레이드오프: 호출 depth가 한 단계 늘지만, Service에 집중된 도메인 로직(검증, 예외 처리)을 일관되게 재사용.
  • 추후 개선 여지: 동시성 제어(낙관적 락), 이벤트 기반 재고 갱신, 쿠폰/결제 도메인 확장

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: apps/commerce-api (Member, Brand, Product, Address, Like, Order)
  • 신규 추가: 6개 도메인 전체 레이어(Entity, Service, Facade, Controller, Repository, Reader, Infrastructure), Admin API, ArchUnit 테스트, AuthUtils
  • 제거/대체: 기존 codeguide 문서 제거, Controller 내 중복 인증 메서드 → AuthUtils로 통합

주요 컴포넌트 책임

Domain Layer

  • Member: 회원가입 검증(loginId/password/name/email/phone/gender), 비밀번호 변경, soft delete 탈퇴
  • Brand: 브랜드 CRUD, likeCount 관리, soft delete 시 하위 상품 cascade
  • Product: 상품 CRUD, 재고 관리(decrease/increaseStock), maxOrderQuantity 검증, likeCount
  • Address: 배송지 CRUD, 기본 배송지 관리(첫 등록 시 자동 기본, 기본 배송지 삭제 불가)
  • Like: 단일 다형 구조(targetType + targetId), Hard delete 토글
  • Order/OrderItem: 주문 생성/취소, 배송지 스냅샷, 상품 스냅샷, 상태 전이(COMPLETED → CANCELLED)

Application Layer

  • OrderFacade: 주문 생성 오케스트레이션 (배송지 검증 → 중복 상품 합산 → 재고 선차감 → 주문 저장)
  • LikeFacade: 좋아요 토글 + Product/Brand likeCount 동기 갱신
  • ProductFacade: 상품 조회 시 Brand 정보 조합
  • BrandFacade: 브랜드 삭제 시 하위 상품 cascade soft delete

Infrastructure Layer

  • Spring Data JPA 기반 Repository 구현, 복잡한 검색/필터/정렬에 QueryDSL 활용 (ProductReaderImpl, MemberReaderImpl, OrderReaderImpl)
  • N+1 방지를 위한 배치 조회 (findAllByIds, findAllByOrderIds)

Support Layer

  • MemberAuthFilter: JWT 인증 필터 (Member API 경로)
  • AdminAuthFilter: X-Loopers-Ldap 헤더 기반 Admin 인증
  • AuthUtils: Controller 공통 인증 로직 추출

API 엔드포인트

Domain Member API Admin API
Member 회원가입, 내 정보 조회, 비밀번호 변경, 전화번호 수정, 탈퇴 회원 목록(검색), 회원 상세
Brand 목록 조회, 상세 조회 등록, 수정, 삭제
Product 목록 조회(검색/필터/정렬), 상세 조회 등록, 수정, 삭제, 재고 수정
Address 목록, 등록, 수정, 삭제, 기본 배송지 설정 -
Like 상품/브랜드 좋아요 토글, 내 좋아요 목록 -
Order 주문 생성, 목록(날짜 필터), 상세, 취소, 배송지 수정 주문 목록(memberId 필터), 주문 상세

테스트 현황

구분 내용
단위 테스트 Entity 불변식, Service 로직 (Fake Repository 활용)
E2E 테스트 전체 API 흐름 (Testcontainers + 실제 HTTP 호출)
ArchUnit 테스트 계층 의존성, Domain Repository 순수성, setter 금지

🔁 Flow Diagram

Main Flow

주문 생성 (핵심 흐름)

sequenceDiagram
  autonumber
  participant Client
  participant Controller
  participant OrderFacade
  participant MemberService
  participant AddressService
  participant ProductService
  participant OrderService
  participant DB

  Client->>Controller: POST /api/v1/orders
  Controller->>OrderFacade: createOrder(loginId, addressId, items)

  OrderFacade->>MemberService: getMember(loginId)
  MemberService->>DB: findByLoginId
  DB-->>MemberService: Member

  OrderFacade->>AddressService: getAddress(addressId, memberId)
  AddressService->>DB: findByIdAndMemberId
  DB-->>AddressService: Address (스냅샷 추출)

  OrderFacade->>OrderService: mergeOrderItems(items)
  Note over OrderService: 동일 상품 수량 합산 + 빈 목록 검증
  OrderService-->>OrderFacade: Map<productId, quantity>

  loop 합산된 상품별
    OrderFacade->>ProductService: getProduct(productId)
    ProductService->>DB: findById
    DB-->>ProductService: Product
    Note over OrderFacade: product.validateOrderQuantity(qty)
    Note over OrderFacade: product.decreaseStock(qty)
  end

  OrderFacade->>OrderService: createOrder(memberId, 배송지 스냅샷, totalAmount)
  OrderService->>DB: save Order
  OrderFacade->>OrderService: createOrderItems(orderId, 상품 스냅샷 목록)
  OrderService->>DB: saveAll OrderItems

  OrderFacade-->>Controller: OrderInfo
  Controller-->>Client: 200 OK + OrderResponse
Loading

주문 취소 (재고 복원)

sequenceDiagram
  autonumber
  participant Client
  participant Controller
  participant OrderFacade
  participant OrderService
  participant ProductService
  participant DB

  Client->>Controller: POST /api/v1/orders/{orderId}/cancel
  Controller->>OrderFacade: cancelOrder(loginId, orderId)

  OrderFacade->>OrderService: cancelOrder(orderId, memberId)
  Note over OrderService: 소유권 검증 + 취소 상태 체크
  Note over OrderService: order.cancel() (COMPLETED → CANCELLED)
  Note over OrderService: OrderItem 목록 조회
  OrderService-->>OrderFacade: List<OrderItem>

  loop 각 아이템별 (cross-domain orchestration)
    OrderFacade->>ProductService: getProduct(productId)
    Note over OrderFacade: product.increaseStock(qty) — 재고 복원
  end

  OrderFacade-->>Controller: void
  Controller-->>Client: 200 OK
Loading

레이어 의존 구조

graph TD
  A[Interfaces Layer<br/>Controller + DTO] -->|호출| B[Application Layer<br/>Facade + Info DTO]
  B -->|호출| C[Domain Layer<br/>Entity + Service + Repository Interface]
  D[Infrastructure Layer<br/>Spring Data JPA + QueryDSL] -->|구현| C
  E[Support Layer<br/>Auth, Error, Config] -.->|cross-cutting| A
  E -.->|cross-cutting| B
  E -.->|cross-cutting| C
  E -.->|cross-cutting| D
Loading

PR 요약: 도메인 주도 설계 기반 이커머스 플랫폼 구현

변경 목적
상품 조회→좋아요→주문으로 이어지는 e-commerce 핵심 흐름을 6개 도메인(Member, Brand, Product, Address, Like, Order)으로 구현하고, DDD 원칙에 따른 계층화된 아키텍처(Domain/Service/Facade/Controller/Repository/Reader/Infrastructure 분리)를 확립하여 비즈니스 로직과 기술 구현의 명확한 경계를 설정합니다.

핵심 변경점

  • Domain Reader 추상화로 Spring Data를 도메인 계층에서 분리하여 의존성 역전 원칙 적용
  • Like: 다형성 단일 엔티티 설계 (targetType + targetId로 Product/Brand 구분), hard-delete 방식
  • Order: 주문 생성 시 상품 및 주소 스냅샷 저장, optimistic all-or-nothing 전략으로 createOrder 루프 내에서 각 상품의 재고를 미리 감소, 주문 취소 시 increaseStock으로 복구
  • OrderFacade의 @transactional이 createOrder/cancelOrder 전체를 감싸므로 원자성 보장(부분 실패 시 재고 감소도 롤백)
  • MemberAuthFilter(JWT) 및 AdminAuthFilter(X-Loopers-Ldap) 추가로 인증/인가 통합
  • 30+ 엔드포인트(회원/브랜드/상품/주소/좋아요/주문 CRUD + Admin 전용)

리스크 및 주의사항

  • OrderFacade의 재고 감소가 루프 내에서 발생하므로, 일부 상품만 차감 후 후속 상품 검증 실패 시 트랜잭션 롤백에 의존 (현재는 안전하지만 향후 비동기 처리 추가 시 재검토 필요)
  • Like의 hard-delete로 인해 통계 데이터(likeCount)가 부정확할 수 있으므로, 좋아요 통계 조회 쿼리와 실제 Like 레코드 수 간 정합성 검증 필요
  • PageResult를 도메인 계층과 Application 계층에서 모두 사용하는데, 이것이 순수 도메인 의존성을 위반하지 않는지 재검토 필요

테스트 및 검증
Unit 테스트(Entity/Service에 대한 도메인 테스트 130+개, 각 Facade별 로직 검증), E2E 테스트(Testcontainers + TestRestTemplate으로 HTTP 시나리오 검증), ArchUnit 규칙(계층 간 접근 제한, 저장소 순수성, 도메인 setter 금지 등) 적용으로 아키텍처 준수 여부를 자동화 검증합니다. OrderFacade cancelOrder의 재고 복구, Like 좋아요/취소 토글, Order 생성 시 재고 전감소 케이스 모두 테스트 커버 확인됨.

juoklee and others added 17 commits February 22, 2026 14:37
- Gender enum(MALE/FEMALE) 추가, phone 필드(선택, 010-XXXX-XXXX) 추가
- PATCH /api/v1/members/me: 전화번호 수정 API
- DELETE /api/v1/members/me: 비밀번호 확인 후 soft delete 회원 탈퇴 API
- 탈퇴 후 로그인 차단(findByLoginIdAndDeletedAtIsNull), 재가입 방지(existsByLoginId)
- DIP 리팩토링: Controller에서 Member Entity 직접 참조 제거, loginId만 전달
- AuthFilter → loginId만 request에 저장, Service에서 @transactional 내 조회로 managed entity 보장
- getPassword() 제거 → verifyPassword() 도메인 메서드로 비밀번호 캡슐화
- 불필요한 save() 호출 제거 (dirty checking 활용)
- ArchUnit: Interfaces → @entity 직접 의존 차단 규칙 추가
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

회원, 주소, 상품, 브랜드, 주문, 좋아요 등 전자상거래의 핵심 도메인을 구현한다. 각 도메인마다 JPA 엔티티, 서비스, 레포지토리/리더, 애플리케이션 Facade, REST 컨트롤러, DTO를 추가한다. 회원 인증을 위해 필터 기반의 보안 레이어를 도입하고, 광범위한 도메인 및 E2E 테스트로 기능을 검증한다.

Changes

Cohort / File(s) Summary
의존성 및 아키텍처 설정
apps/commerce-api/build.gradle.kts, apps/commerce-api/src/test/java/com/loopers/ArchitectureTest.java
Spring Security 의존성 추가 및 계층화 아키텍처 제약(Interfaces → Application → Domain/Infrastructure → Support) 검증 규칙 도입. Domain 엔티티의 public setter 금지, Application의 JPA 어노테이션 사용 금지.
도메인: Member (회원)
src/main/java/com/loopers/domain/member/Member.java, MemberReader.java, MemberRepository.java, MemberService.java, Gender.java, PasswordEncoder.java
회원 엔티티, 로그인ID 중복 검증, 비밀번호 인코딩, 전화번호/비밀번호 변경, 회원탈퇴(soft delete) 기능 구현.
도메인: Address (배송주소)
src/main/java/com/loopers/domain/address/Address.java, AddressReader.java, AddressRepository.java, AddressService.java
회원당 최대 10개 주소 관리, 첫 주소 자동 기본값 설정, 기본값 변경, soft delete 방지.
도메인: Brand (브랜드)
src/main/java/com/loopers/domain/brand/Brand.java, BrandReader.java, BrandRepository.java, BrandService.java
브랜드 CRUD, 이름 중복 검증, 좋아요 카운트 증감.
도메인: Product (상품)
src/main/java/com/loopers/domain/product/Product.java, ProductReader.java, ProductRepository.java, ProductService.java, ProductSortType.java
상품 CRUD, 재고 관리, 최대주문수량 검증, 좋아요 카운트, LATEST/PRICE_ASC/PRICE_DESC/LIKES_DESC 정렬.
도메인: Order (주문)
src/main/java/com/loopers/domain/order/Order.java, OrderItem.java, OrderReader.java, OrderRepository.java, OrderService.java, OrderStatus.java
주문 생성/취소, 배송지 수정, 주문상태(COMPLETED/CANCELLED) 전이 제약, 재고 감소.
도메인: Like (좋아요)
src/main/java/com/loopers/domain/like/Like.java, LikeReader.java, LikeRepository.java, LikeService.java, LikeTargetType.java
상품/브랜드 좋아요 토글(중복 제약), PRODUCT/BRAND 타입 구분.
도메인 공통
src/main/java/com/loopers/domain/PageResult.java
페이징 결과 불변 레코드: content, totalElements, totalPages, page, size.
인프라: Member 구현
src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java, MemberReaderImpl.java, MemberRepositoryImpl.java, PasswordEncoderImpl.java
QueryDSL 기반 동적 검색(loginId, name, email 키워드), BCrypt 패스워드 인코딩.
인프라: Address 구현
src/main/java/com/loopers/infrastructure/address/AddressJpaRepository.java, AddressReaderImpl.java, AddressRepositoryImpl.java
soft delete 필터링(DeletedAtIsNull) 적용.
인프라: Brand 구현
src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java, BrandReaderImpl.java, BrandRepositoryImpl.java
브랜드명 부분 검색, 이름 중복 검증, soft delete 필터링.
인프라: Product 구현
src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java, ProductReaderImpl.java, ProductRepositoryImpl.java
QueryDSL 동적 쿼리, 키워드/브랜드ID 필터링, ProductSortType별 정렬 해석.
인프라: Order 구현
src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java, OrderReaderImpl.java, OrderRepositoryImpl.java, OrderItemJpaRepository.java, OrderItemReaderImpl.java, OrderItemRepositoryImpl.java
주문 및 주문항목 조회/저장, 날짜 범위 필터링.
인프라: Like 구현
src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java, LikeReaderImpl.java, LikeRepositoryImpl.java
회원/타입/타겟ID 기반 좋아요 조회.
애플리케이션: Facade 계층
src/main/java/com/loopers/application/member/MemberFacade.java, AddressFacade.java, BrandFacade.java, ProductFacade.java, OrderFacade.java, LikeFacade.java
서비스 조율, DTO 변환, 비즈니스 로직 조직. MemberFacade는 회원별 데이터 조회, 주소/상품/주문 Facade는 엔티티 변환 및 페이징 처리.
애플리케이션: Info/DTO
src/main/java/com/loopers/application/.../...Info.java
PagedInfo, AddressInfo, BrandInfo, ProductInfo, OrderInfo, OrderItemInfo, OrderSummaryInfo, MemberInfo, AdminMemberInfo, BrandLikeInfo, ProductLikeInfo, LikeToggleInfo. 도메인 → 애플리케이션 계층 변환.
REST API: Member
src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java, MemberAdminV1Controller.java, MemberV1Dto.java, MemberAdminV1Dto.java
회원 등록/조회/비밀번호변경/전화번호수정/탈퇴, 관리자 회원 목록/상세 조회.
REST API: Address
src/main/java/com/loopers/interfaces/api/address/AddressV1Controller.java, AddressV1Dto.java
배송주소 CRUD, 기본값 변경.
REST API: Brand
src/main/java/com/loopers/interfaces/api/brand/BrandV1Controller.java, BrandAdminV1Controller.java, BrandV1Dto.java
브랜드 목록/상세 조회(공개), 관리자 CRUD.
REST API: Product
src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java, ProductAdminV1Controller.java, ProductV1Dto.java
상품 목록/상세 조회(검색/필터링/정렬), 관리자 CRUD/재고 수정.
REST API: Order
src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java, OrderAdminV1Controller.java, OrderV1Dto.java
주문 생성/조회/취소/배송지수정, 관리자 주문 조회.
REST API: Like
src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java, LikeV1Dto.java
상품/브랜드 좋아요 토글, 좋아요한 상품/브랜드 조회.
보안 및 인증
src/main/java/com/loopers/support/auth/MemberAuthFilter.java, AdminAuthFilter.java, AuthUtils.java, src/main/java/com/loopers/support/config/SecurityConfig.java, src/main/java/com/loopers/support/error/ErrorType.java
X-Loopers-LoginId/LoginPw 헤더 기반 회원 인증, X-Loopers-Ldap 헤더 기반 관리자 인증, UNAUTHORIZED ErrorType 추가.
도메인 테스트
src/test/java/com/loopers/domain/.../...Test.java
Member, Address, Brand, Product, Order, OrderItem, Like 엔티티 및 서비스 유닛 테스트(validation, state transitions, error handling).
API E2E 테스트
src/test/java/com/loopers/interfaces/api/...V1ApiE2ETest.java
Member, Address, Brand, Product, Order, Like 컨트롤러의 통합 테스트(CRUD, 권한 검증, 에러 케이스). 관리자 인증 테스트.
HTTP 스펙 문서
http/commerce-api/*.http
member-v1, address-v1, brand-v1, product-v1, order-v1, like-v1 API 요청/응답 예시.

Sequence Diagram

sequenceDiagram
    participant Client as 클라이언트
    participant Controller as OrderV1Controller
    participant Facade as OrderFacade
    participant MemberService as MemberService
    participant AddressService as AddressService
    participant ProductService as ProductService
    participant OrderService as OrderService
    
    Client->>Controller: POST /api/v1/orders (loginId, addressId, items)
    Controller->>Facade: createOrder(loginId, addressId, itemRequests)
    Facade->>MemberService: getMemberByLoginId(loginId)
    MemberService-->>Facade: Member
    Facade->>AddressService: getAddress(memberId, addressId)
    AddressService-->>Facade: Address
    Facade->>ProductService: getProductsByIds(productIds)
    ProductService-->>Facade: List<Product>
    Facade->>Facade: validateStock(items, products)
    Facade->>ProductService: decreaseStock(productId, quantity)
    ProductService-->>Facade: ✓
    Facade->>OrderService: createOrder(memberId, shipping, totalAmount)
    OrderService-->>Facade: Order
    Facade->>OrderService: createOrderItems(orderId, items)
    OrderService-->>Facade: ✓
    Facade->>Facade: toOrderInfo(order, items)
    Facade-->>Controller: OrderInfo
    Controller-->>Client: ApiResponse<OrderV1Dto.OrderResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

이유:

  • 6개의 새로운 도메인(Member, Address, Brand, Product, Order, Like) 각각이 엔티티, 서비스, 레포지토리, Facade, 컨트롤러, 테스트를 포함하므로 이질적 변화가 많다.
  • 각 도메인마다 검증 규칙, 상태 전이, soft delete, 권한 검증 등이 서로 다른 로직을 요구한다.
  • 회원 인증(필터 기반), 관리자 인증, 비밀번호 인코딩 등 보안 계층이 새로 도입되어 상호작용 복잡도가 높다.
  • 주문 생성 흐름에서 여러 서비스(Member, Address, Product, Order)가 협력하므로 트랜잭션 경계와 에러 처리를 세밀하게 검토해야 한다.
  • 파일 개수가 100개 이상이고 새로운 것들이므로 전체 적용 범위가 크다.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 주요 변경사항인 도메인 주도 설계 구현을 명확히 나타내며, 핵심 의도를 잘 전달한다.
Description check ✅ Passed Pull request description is comprehensive with summary, design context, component responsibilities, and flow diagrams.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@juoklee juoklee changed the base branch from main to juoklee February 26, 2026 16:11
@juoklee juoklee changed the title [3주차] 도메인 주도 설계 구현 [3주차] 도메인 주도 설계 구현 - 이주옥 Feb 26, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (26)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java-547-567 (1)

547-567: ⚠️ Potential issue | 🟠 Major

헬퍼 메서드가 실패 원인을 NPE로 은닉한다

운영 관점에서 사전 데이터 생성이 실패했을 때 NPE로만 보이면 CI 장애 원인 파악 시간이 길어져 대응 속도가 떨어진다. registerBrand/registerProduct에서 CREATEDbody/data 존재를 먼저 단언한 뒤 ID를 반환하도록 수정하는 것이 안전하다. 추가 테스트로, 의도적으로 잘못된 생성 요청을 넣었을 때 NPE가 아니라 상태코드 단언 실패로 종료되는지 검증해야 한다.

수정 예시
 private Long registerBrand(String name, String description) {
     var request = new BrandV1Dto.RegisterRequest(name, description);
     ResponseEntity<ApiResponse<BrandV1Dto.BrandResponse>> response = testRestTemplate.exchange(
         BRAND_ADMIN,
         HttpMethod.POST,
         adminEntity(request),
         new ParameterizedTypeReference<>() {}
     );
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+    assertThat(response.getBody()).isNotNull();
+    assertThat(response.getBody().data()).isNotNull();
     return response.getBody().data().brand().id();
 }

 private Long registerProduct(Long brandId, String name, Long price, int stock, int maxOrder) {
     var request = new ProductV1Dto.RegisterRequest(brandId, name, "설명", price, stock, maxOrder);
     ResponseEntity<ApiResponse<ProductV1Dto.ProductResponse>> response = testRestTemplate.exchange(
         PRODUCT_ADMIN,
         HttpMethod.POST,
         adminEntity(request),
         new ParameterizedTypeReference<>() {}
     );
+    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+    assertThat(response.getBody()).isNotNull();
+    assertThat(response.getBody().data()).isNotNull();
     return response.getBody().data().product().id();
 }

As per coding guidelines, "예외 흐름이 명확한지 점검한다. null 처리 ... 점검한다."

🤖 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 547 - 567, The helper methods registerBrand and registerProduct
currently assume response body exists and can NPE; update both methods to first
assert the response status is HttpStatus.CREATED (or expected status) and that
response.getBody() and response.getBody().data() (and .brand()/ .product()) are
non-null before extracting the id, throwing clear assertion failures if not;
additionally add a negative test that sends an invalid register request and
asserts the response status assertion fails (e.g., BAD_REQUEST) to ensure
failures surface as status assertion errors rather than NPEs.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java-9-30 (1)

9-30: ⚠️ Potential issue | 🟠 Major

비밀번호 필드를 가진 record 요청 DTO는 자동 toString()으로 민감정보를 노출한다.

Line 9-30의 RegisterRequest, ChangePasswordRequest, WithdrawRequest는 record 기본 toString()이 원문 비밀번호를 포함하므로, 요청 로깅/예외 로깅 시 계정 탈취 리스크가 발생한다. 수정안은 민감 필드를 마스킹하는 toString() 오버라이드 또는 비밀번호 필드를 가진 요청 DTO만 일반 클래스로 전환하여 로그 출력을 통제하는 방식이 적절하다. 추가 테스트로는 각 DTO의 toString() 결과에 평문 비밀번호가 포함되지 않는지 검증해야 한다.

수정 예시 diff
     public record RegisterRequest(
         String loginId,
         String password,
         String name,
         LocalDate birthDate,
         String gender,
         String email,
         String phone
-    ) {}
+    ) {
+        `@Override`
+        public String toString() {
+            return "RegisterRequest[loginId=%s, password=****, name=%s, birthDate=%s, gender=%s, email=%s, phone=%s]"
+                .formatted(loginId, name, birthDate, gender, email, phone);
+        }
+    }
 
     public record ChangePasswordRequest(
         String currentPassword,
         String newPassword
-    ) {}
+    ) {
+        `@Override`
+        public String toString() {
+            return "ChangePasswordRequest[currentPassword=****, newPassword=****]";
+        }
+    }
 
     public record WithdrawRequest(
         String password
-    ) {}
+    ) {
+        `@Override`
+        public String toString() {
+            return "WithdrawRequest[password=****]";
+        }
+    }

As per coding guidelines **/*.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/interfaces/api/member/MemberV1Dto.java`
around lines 9 - 30, The record DTOs RegisterRequest, ChangePasswordRequest, and
WithdrawRequest expose plaintext passwords via the default record toString();
update them to prevent sensitive leakage by either converting those three
records into ordinary classes with a custom toString() that masks password
fields (e.g., replace characters with asterisks or omit them) or override
toString() for each record to return a safe representation that excludes or
masks password values; update any constructors/getters as needed to preserve
immutability/validation and add unit tests asserting that toString() for
RegisterRequest, ChangePasswordRequest, and WithdrawRequest does not contain the
raw password string.
apps/commerce-api/src/main/java/com/loopers/domain/PageResult.java-5-11 (1)

5-11: ⚠️ Potential issue | 🟠 Major

도메인 페이징 객체가 가변 컬렉션 참조를 그대로 노출하고 있어 상태 오염 위험이 있다.

Line 6의 content가 방어적 복사 없이 저장되면 생성 이후 외부 변경이 내부 상태를 오염시켜 캐시/응답 일관성이 깨질 수 있어 운영 장애 원인이 된다. compact constructor에서 List.copyOf와 기본 메타데이터 검증을 추가하는 수정이 필요하다. 추가 테스트로는 원본 리스트 변경 후 PageResult.content 불변성 유지, null content 입력 시 예외, 음수 page/size/total 입력 시 예외를 포함해야 한다.

수정 예시 diff
 package com.loopers.domain;
 
 import java.util.List;
+import java.util.Objects;
 
 public record PageResult<T>(
     List<T> content,
     long totalElements,
     int totalPages,
     int page,
     int size
-) {}
+) {
+    public PageResult {
+        content = List.copyOf(Objects.requireNonNull(content, "content must not be null"));
+        if (totalElements < 0 || totalPages < 0 || page < 0 || size < 0) {
+            throw new IllegalArgumentException("paging metadata must be non-negative");
+        }
+    }
+}

As per coding guidelines **/*.java의 "null 처리, 방어적 복사, 불변성 ... 점검한다" 및 **/domain/**/*.java의 "불변성과 캡슐화를 점검한다" 기준을 적용했다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/commerce-api/src/main/java/com/loopers/domain/PageResult.java` around
lines 5 - 11, PageResult currently stores the mutable List<T> content reference
directly; add a compact constructor in the PageResult record that performs
defensive copy via List.copyOf(content) and validates inputs (throw
NullPointerException for null content and IllegalArgumentException for negative
totalElements/page/size/totalPages as applicable) to enforce immutability and
metadata correctness, update the record signature to use the compact constructor
name PageResult and ensure content is assigned to the copied list; also add unit
tests: "original list mutation does not change PageResult.content", "null
content throws NPE", and "negative page/size/totalElements throws IAE".
apps/commerce-api/build.gradle.kts-13-14 (1)

13-14: ⚠️ Potential issue | 🟠 Major

보안 의존성의 명시적 버전 선언과 취약점 스캔 CI 게이트를 추가해야 한다.

14번 라인의 spring-boot-starter-security는 버전이 명시되지 않아 부모 Spring Boot 버전에 종속된다. 2025년 Spring Security에서 발표된 CVE-2025-22228(HIGH, BCrypt 우회), CVE-2025-22234(MEDIUM, 타이밍 공격), CVE-2025-41248/41249(권한 검증 우회)가 존재하므로, 프로젝트가 사용 중인 Spring Boot 버전의 Spring Security 버전을 명확히 확인하고 취약 버전이 아님을 검증해야 한다. 아울러 23번 라인의 archunit-junit5:1.4.0은 알려진 취약점이 없으나, 최신 버전 1.4.1(2025년 5월)로 업그레이드하는 편이 바람직하다.

운영 관점의 수정안:

  • spring-boot-starter-security의 부모 Spring Boot 버전을 명시적으로 확인하고 Spring Security ≥ 6.3.9 또는 ≥ 6.4.5 이상으로 고정한다.
  • archunit-junit5를 1.4.1로 업그레이드한다.
  • CI에 소프트웨어 구성 분석(SCA) 스캔을 추가하여 의존성 취약점 발견 시 빌드를 차단하도록 설정한다.

추가 테스트: PR 단계에서 알려진 Spring Security 취약점 버전으로의 의존성 침투 시 CI 스캔이 정상적으로 차단하는지 검증한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/commerce-api/build.gradle.kts` around lines 13 - 14, Pin the Spring
Security dependency version instead of relying on the parent Spring Boot BOM by
replacing the loose dependency
implementation("org.springframework.boot:spring-boot-starter-security") with an
explicit artifact/version that ensures Spring Security >= 6.3.9 or >= 6.4.5
(confirm the exact safe version for your Spring Boot line) and update the
archunit test dependency from version 1.4.0 to 1.4.1 (reference: the existing
archunit-junit5 declaration). Also add/enable an SCA-based dependency
vulnerability scan in the CI pipeline so builds fail on known vulnerable
versions and include a PR-level test that validates the CI gate blocks a PR when
a vulnerable Spring Security version is introduced.
http/commerce-api/like-v1.http-9-10 (1)

9-10: ⚠️ Potential issue | 🟠 Major

민감 인증정보를 HTTP 샘플에 하드코딩하면 유출 위험이 커진다

Line 9-10 등에서 로그인 ID/PW가 평문으로 반복되어 운영 로그, 화면 캡처, 협업 도구 공유 시 유출 위험이 커진다. 샘플은 환경변수/로컬 시크릿 참조로 바꾸는 것이 안전하다. 추가 테스트로 CI에서 X-Loopers-LoginPw: 뒤 리터럴 문자열을 탐지하는 정적 점검을 넣어 재유입을 차단하는 것이 좋다.

🔧 제안 수정안
 `@baseUrl` = http://localhost:8080
+@loginId = {{$dotenv LOOPERS_LOGIN_ID}}
+@loginPw = {{$dotenv LOOPERS_LOGIN_PW}}

@@
-X-Loopers-LoginId: testUser1
-X-Loopers-LoginPw: Test1234!
+X-Loopers-LoginId: {{loginId}}
+X-Loopers-LoginPw: {{loginPw}}

Also applies to: 14-15, 19-20, 31-32, 36-37, 41-42, 53-54, 58-59, 70-71, 75-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@http/commerce-api/like-v1.http` around lines 9 - 10, Replace the hardcoded
credentials in the HTTP sample headers (X-Loopers-LoginId and X-Loopers-LoginPw)
with placeholder references to environment/local secrets (e.g.,
${LOOPERS_LOGIN_ID} and ${LOOPERS_LOGIN_PW}) and update any sample usage
comments to instruct users to set those env vars or a local secret file; then
add a CI static-check rule that scans .http samples for literal strings
following "X-Loopers-LoginPw:" (and optionally "X-Loopers-LoginId:") to fail the
build if a plaintext credential is detected, ensuring future commits cannot
reintroduce hardcoded credentials.
apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeReaderImpl.java-26-28 (1)

26-28: ⚠️ Potential issue | 🟠 Major

페이지 파라미터 검증이 없어 500 계열 예외로 새어 나갈 수 있다

Line 27의 PageRequest.of(page, size)는 음수/0 입력 시 IllegalArgumentException을 던져 공통 에러 포맷을 깨뜨릴 수 있다. 운영 관점에서 클라이언트 입력 오류가 500으로 관측되면 장애 탐지 노이즈가 커진다. 리더 진입부에서 CoreException(BAD_REQUEST)로 선검증해 일관된 에러 응답을 보장하는 것이 필요하다. 추가 테스트로 page=-1, size=0 요청 시 400과 표준 에러 바디를 검증하는 API 테스트를 추가하는 것이 좋다.

🛠️ 제안 수정안
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
@@
     public PageResult<Like> findAllByMemberIdAndTargetType(Long memberId, LikeTargetType targetType, int page, int size) {
+        if (page < 0 || size <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "페이지 파라미터가 올바르지 않다.");
+        }
         Pageable pageable = PageRequest.of(page, size);
         Page<Like> result = likeJpaRepository.findAllByMemberIdAndTargetType(memberId, targetType, pageable);
         return new PageResult<>(result.getContent(), result.getTotalElements(),
             result.getTotalPages(), result.getNumber(), result.getSize());
     }

Based on learnings In the loop-pack-be-l2-vol3-java project, enforce unified error handling by routing errors through CoreException to 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/infrastructure/like/LikeReaderImpl.java`
around lines 26 - 28, The method findAllByMemberIdAndTargetType in
LikeReaderImpl calls PageRequest.of(page, size) without validating inputs, which
can raise IllegalArgumentException and leak 500 errors; add explicit validation
at the start of findAllByMemberIdAndTargetType to ensure page >= 0 and size > 0
and throw new CoreException(BAD_REQUEST) for invalid values so errors flow
through ApiControllerAdvice, and add API tests that call the endpoint (or the
reader) with page=-1 and size=0 asserting a 400 response and the standardized
error body.
apps/commerce-api/src/main/java/com/loopers/support/auth/AdminAuthFilter.java-53-54 (1)

53-54: ⚠️ Potential issue | 🟠 Major

startsWith("/api-admin/") 경로 판별은 우회 여지가 있다

Line 54의 현재 구현은 /api-admin(슬래시 없음) 경로를 필터링하지 못한다. 실제 운영 환경에서 향후 개발자가 /api-admin 엔드포인트를 추가할 경우 인증 필터가 우회될 위험이 있으며, 이는 재현이 어려운 보안 사고로 이어질 수 있다. 현재 모든 엔드포인트가 /api-admin/v1/... 패턴을 따르고 있지만, 보안 필터는 명시적 규칙에 의존해야 한다. RequestMatcher를 사용하여 /api-admin/api-admin/** 두 경로를 함께 명시하는 것이 방어적 설계다. 추가로 /api-admin, /api-admin/, /api-admin/v1/brands 세 경로에서 동일한 인증 동작을 검증하는 테스트를 추가하는 것이 좋다.

🧭 제안 수정안
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
@@
 public class AdminAuthFilter extends OncePerRequestFilter {
+    private static final RequestMatcher ADMIN_MATCHER = new OrRequestMatcher(
+        new AntPathRequestMatcher("/api-admin"),
+        new AntPathRequestMatcher("/api-admin/**")
+    );
@@
     private boolean requiresAuthentication(HttpServletRequest request) {
-        return request.getRequestURI().startsWith("/api-admin/");
+        return ADMIN_MATCHER.matches(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/support/auth/AdminAuthFilter.java`
around lines 53 - 54, The requiresAuthentication(HttpServletRequest request)
method currently uses request.getRequestURI().startsWith("/api-admin/") which
misses the root path "/api-admin" and can be bypassed; update this method to
match both "/api-admin" and any subpaths (e.g., "/api-admin/**") using a
RequestMatcher (or explicitly check equals("/api-admin") ||
startsWith("/api-admin/")), and add unit tests asserting the filter triggers for
"/api-admin", "/api-admin/", and "/api-admin/v1/brands" to ensure consistent
authentication behavior.
apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java-25-27 (1)

25-27: ⚠️ Potential issue | 🟠 Major

인가 기본 정책을 permitAll로 설정했으므로, 신규 보호 API 경로 추가 시 필터 규칙 누락만으로 무인증 노출이 발생할 수 있다.

AdminAuthFilterMemberAuthFilter에서 특정 경로만 인증을 검증하는 구조이므로, 새로운 API 엔드포인트가 추가될 때 필터의 requiresAuthentication() 메서드에 경로 규칙을 명시하지 않으면 자동으로 permitAll() 정책에 따라 무방비로 노출된다. 이는 운영 과정에서 보호 대상 경로 누락으로 인한 보안 결함이 발생하기 쉬운 fail-open 구조다.

더불어 두 필터 모두 인증 성공 시 SecurityContextAuthentication 객체를 설정하지 않고, request.setAttribute()로만 정보를 저장하므로 Spring Security의 인가 규칙이 실제로 작동하지 않는 상태다.

수정 방향:

  • SecurityConfig의 기본 정책을 denyAll()로 변경하고 인증 불필요 경로만 permitAll()로 명시하거나, 보호 경로를 선언적으로 구성해 새 엔드포인트 추가 시 기본 보호 상태를 유지하도록 개선한다.
  • 필터에서 인증 성공 시 Spring의 Authentication 객체를 SecurityContext에 설정해 인가 규칙이 정상 작동하도록 한다.
  • 보호 대상 URI(/api/v1/members/**, /api/v1/orders/**, /api/v1/products/{id}/likes 등)에 대해 무헤더 및 오류 헤더 요청은 항상 401을 반환하고, 정상 인증 헤더 요청만 통과하는 파라미터화 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/support/config/SecurityConfig.java`
around lines 25 - 27, SecurityConfig currently calls
authorizeHttpRequests(...).anyRequest().permitAll which creates a fail-open
policy; change the default to denyAll() and explicitly permit only public paths
(e.g., health, auth endpoints) so new APIs are protected by default, and declare
protected URIs (/api/v1/members/**, /api/v1/orders/**, /api/v1/products/*/likes)
explicitly in the security matcher rules. In AdminAuthFilter and
MemberAuthFilter ensure requiresAuthentication() implements strict path matching
for only the intended protected paths and, on successful authentication,
populate Spring Security by creating an Authentication and calling
SecurityContextHolder.getContext().setAuthentication(auth) instead of only using
request.setAttribute(...). Finally add parameterized E2E tests that hit the
protected URIs to assert requests with missing/invalid auth return 401 and valid
auth is allowed.
apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java-41-48 (1)

41-48: ⚠️ Potential issue | 🟠 Major

likeCount 증감 로직의 동시성 보호가 부재하여 고트래픽 환경에서 유실 갱신 발생 가능하다.

현재 구현은 엔티티를 로드한 후 메모리에서 증감하고 저장하는 Read-Modify-Write 패턴을 사용한다. 이 방식은 두 개 이상의 트랜잭션이 동일 Brand를 동시에 수정할 때 일부 변경사항이 누락되는 유실 갱신 문제를 야기한다. 결과적으로 Brand.likeCount와 실제 Like 레코드 건수가 불일치하여 운영 지표 신뢰도와 정렬 결과의 안정성이 저하된다.

  • 수정안: 데이터베이스 수준의 원자적 쿼리(UPDATE brand SET like_count = like_count + 1 WHERE id = ?)를 통해 동시 요청에도 카운트 일관성을 보장해야 한다.
  • 추가 테스트: 동시성 통합 테스트(최소 100개 이상의 동시 toggleBrandLike 요청)를 실시하여 최종 Brand.likeCount와 Like 레코드 건수가 일치하는지 검증해야 한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java` around
lines 41 - 48, The current in-memory read-modify-write in
Brand.increaseLikeCount() and Brand.decreaseLikeCount() causes lost updates
under concurrency; replace these usages with a database-level atomic update
(e.g., add repository methods like incrementLikeCountById(id) and
decrementLikeCountById(id) that execute SQL UPDATE brand SET like_count =
like_count + 1 (or -1 with safety) WHERE id = ? and call those instead of
modifying Brand.likeCount in memory), ensure decrement protects against negative
values in the SQL (e.g., WHERE like_count > 0), and add a concurrency
integration test that issues 100+ concurrent toggleBrandLike requests and
asserts final Brand.likeCount equals the Like record count.
apps/commerce-api/src/test/java/com/loopers/support/auth/AdminAuthFilterE2ETest.java-72-74 (1)

72-74: ⚠️ Potential issue | 🟠 Major

성공 케이스에서 상태코드를 isNotEqualTo(401)로만 검증하면 404/500도 통과되어 실제 API 동작 검증이 불완전하다.

인증 필터가 의도대로 통과하더라도 라우팅 오류나 내부 오류로 인한 4xx/5xx 응답이 발생하면 테스트는 녹색으로 유지되어 보안 회귀 탐지가 늦어진다. 특히 Admin 엔드포인트는 인증 필터 검증이 핵심이므로 성공 경로에서 기대하는 상태코드를 명시해야 한다.

  • 라인 72-74: passesAuthentication_whenValidLdapHeader() - 유효 LDAP 헤더로 인증 통과 시 반드시 2xx 상태코드 명시
  • 라인 86-87: allowsPublicApi_withoutLdapHeader() - Public API 접근 성공 시 2xx 명시
🛠️ 수정안
-        assertThat(response.getStatusCode()).isNotEqualTo(HttpStatus.UNAUTHORIZED);
+        assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
🤖 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/support/auth/AdminAuthFilterE2ETest.java`
around lines 72 - 74, Replace the weak negative assertion in
passesAuthentication_whenValidLdapHeader() (and similarly in
allowsPublicApi_withoutLdapHeader()) that only checks
"isNotEqualTo(HttpStatus.UNAUTHORIZED)" with a positive assertion that the
response is a successful 2xx, e.g. assert that
response.getStatusCode().is2xxSuccessful() (or
assertTrue(response.getStatusCode().is2xxSuccessful())) to ensure the admin auth
filter truly allows the request and avoids false positives from 4xx/5xx routing
or server errors.
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java-49-50 (1)

49-50: ⚠️ Potential issue | 🟠 Major

소계 계산에서 long 오버플로우를 방지해야 한다.

productPrice(Long) × quantity(int) 곱셈 결과가 Long.MAX_VALUE를 초과하면 음수 또는 왜곡된 금액이 저장되어 결제·정산 장애로 이어진다. 예를 들어 상품 가격이 약 4.6조원이고 수량이 3개인 경우 오버플로우가 발생한다.

  • 수정안: Math.multiplyExact(productPrice, (long) quantity)를 사용하고, 오버플로우 발생 시 CoreException(ErrorType.BAD_REQUEST, ...)로 변환해 요청을 거부해야 한다.
  • 추가 테스트: Long.MAX_VALUE / 2 근처 가격과 중대형 수량의 조합에 대해 오버플로우 예외 케이스를 분리해 검증해야 한다. 예: productPrice = Long.MAX_VALUE / 3 + 1, quantity = 3일 때 ArithmeticException 발생 확인.
🛠️ 수정 예시
 public Long getSubtotal() {
-    return productPrice * quantity;
+    try {
+        return Math.multiplyExact(productPrice, (long) quantity);
+    } catch (ArithmeticException 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/domain/order/OrderItem.java`
around lines 49 - 50, The getSubtotal method currently multiplies productPrice
by quantity without overflow checks; replace the multiplication with
Math.multiplyExact(productPrice, (long) quantity) inside getSubtotal and catch
ArithmeticException to throw new CoreException(ErrorType.BAD_REQUEST, "subtotal
overflow" or similar) so overflow requests are rejected; update tests to include
an overflow case (e.g., productPrice = Long.MAX_VALUE / 3 + 1 and quantity = 3)
to assert ArithmeticException handling/translation to CoreException.
apps/commerce-api/src/main/java/com/loopers/domain/address/Address.java-84-86 (1)

84-86: 🛠️ Refactor suggestion | 🟠 Major

도메인 불변식을 엔티티 레이어에서 보호해야 한다.

현재 구현에서 changeDefault(boolean isDefault) 메서드는 공개(public)되어 있으나 어떤 검증도 수행하지 않는다. 실제로는 AddressService.changeDefault()가 같은 트랜잭션 내에서 기존 기본 배송지를 해제하고 새로운 기본 배송지를 설정하므로 회원이 기본 배송지 없는 상태가 되지 않지만, 도메인 불변식 "모든 회원은 최소 하나의 기본 배송지를 가져야 한다"는 엔티티 레이어에서 보호되지 않고 있다.

공개 뮤테이터 메서드가 검증 없이 노출되어 있으면 서비스 레이어 규율에만 의존하게 되어 도메인 설계 원칙에 어긋난다. changeDefault()는 패키지 프라이빗으로 제한하거나, 도메인 규칙을 메서드에 포함시켜야 한다. 예를 들어 setAsDefault() / unsetAsDefault() 같은 의도를 명확히 하는 분리된 메서드로 리팩토링하고, 각각 해당 불변식을 검증하도록 개선이 필요하다.

🤖 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/address/Address.java`
around lines 84 - 86, The public mutator changeDefault(boolean isDefault)
exposes domain-invariant risk; restrict its visibility and enforce the "each
member must have at least one default address" rule inside the entity. Refactor
changeDefault into two intention-revealing methods on Address: setAsDefault()
and unsetAsDefault(), make them package-private (or private) so only
domain/service code in the same package can call them, and add validation hooks
inside unsetAsDefault() to prevent leaving a member without any default (or
throw a domain exception if called incorrectly). Update
AddressService.changeDefault() to call the new setAsDefault()/unsetAsDefault()
methods and remove reliance on a raw public boolean setter.
apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java-46-51 (1)

46-51: ⚠️ Potential issue | 🟠 Major

brandId 도메인 검증 누락으로 실패 지점이 인프라로 밀린다.

운영 관점에서 brandId가 null/비정상 값이어도 엔티티 생성이 통과하면 트랜잭션 커밋 시점의 DB 예외로 터져 비즈니스 오류와 시스템 오류가 혼재될 수 있다.
수정안으로 생성 시점에 brandId를 양수/필수로 검증해 도메인 오류로 조기 차단해야 한다.
추가 테스트로 brandIdnull, 0, 음수인 경우 CoreException(BAD_REQUEST)가 발생하는지 검증해야 한다.

수정 예시
 public static Product create(Long brandId, String name, String description, Long price, int stockQuantity, int maxOrderQuantity) {
+    validatePositive(brandId, "브랜드 ID는 0보다 커야 합니다.");
     validateNotBlank(name, "상품명은 필수입니다.");
     validatePositive(price, "가격은 0보다 커야 합니다.");
     validateNotNegative(stockQuantity, "재고 수량은 0 이상이어야 합니다.");
     validatePositive((long) maxOrderQuantity, "최대 주문 수량은 0보다 커야 합니다.");
     return new Product(brandId, name, description, price, stockQuantity, maxOrderQuantity);
 }

As per coding guidelines **/domain/**/*.java: 도메인 모델의 규칙과 불변성을 엔티티 경계에서 명확히 보장해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java`
around lines 46 - 51, Product.create currently lacks validation for brandId so
invalid/null/zero/negative values propagate to infra-level DB errors; add
domain-level checks by validating brandId is non-null and positive at the start
of Product.create (use the existing validation helpers or add
validateNotNull/validatePositive calls) before constructing Product, and
update/create unit tests to assert that Product.create throws CoreException with
BAD_REQUEST for brandId == null, 0, and negative values; reference the
Product.create factory method and the Product constructor when making the
change.
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java-57-69 (1)

57-69: ⚠️ Potential issue | 🟠 Major

좋아요 수 증감 시 동시성 문제

increaseLikeCountdecreaseLikeCount는 조회 후 엔티티 필드를 변경하는 방식으로, 동시에 여러 요청이 들어오면 lost update가 발생할 수 있다. 예를 들어 두 사용자가 동시에 좋아요를 누르면 실제로는 1만 증가할 수 있다.

운영 환경에서 동시성 이슈를 방지하려면:

  1. @Version 필드를 통한 낙관적 락 적용
  2. 또는 UPDATE product SET like_count = like_count + 1 WHERE id = ? 형태의 atomic update 쿼리 사용
🔧 atomic update 쿼리 방식 예시

ProductJpaRepository에 추가:

`@Modifying`
`@Query`("UPDATE Product p SET p.likeCount = p.likeCount + 1 WHERE p.id = :id")
int incrementLikeCount(`@Param`("id") Long id);

`@Modifying`
`@Query`("UPDATE Product p SET p.likeCount = CASE WHEN p.likeCount > 0 THEN p.likeCount - 1 ELSE 0 END WHERE p.id = :id")
int decrementLikeCount(`@Param`("id") Long id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java`
around lines 57 - 69, The current increaseLikeCount and decreaseLikeCount
methods perform read-modify-write on the Product entity and can suffer lost
updates under concurrency; replace their logic with atomic DB updates by adding
modifying repository methods (e.g., in ProductJpaRepository add
incrementLikeCount(Long id) and decrementLikeCount(Long id) that run UPDATE ...
SET likeCount = likeCount + 1 / CASE ... -1) and call those from the service
methods instead of loading Product via getProduct(id); ensure the service
methods remain `@Transactional` and use the repository methods' returned
updated-row count to handle missing records.
apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java-49-55 (1)

49-55: ⚠️ Potential issue | 🟠 Major

대량 브랜드 삭제 시 트랜잭션 크기 문제

deleteAllByBrandId는 단일 트랜잭션 내에서 모든 상품을 조회하고 각각 soft delete를 수행한다. 브랜드에 속한 상품이 수천 개 이상이면:

  1. 영속성 컨텍스트에 대량 엔티티 로딩으로 메모리 부하
  2. flush 시점에 대량 UPDATE 쿼리 발생으로 DB 부하
  3. 트랜잭션 시간 증가로 락 경합 가능성

배치 처리(청크 단위 삭제) 또는 벌크 UPDATE 쿼리로 개선을 검토해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java`
around lines 49 - 55, The deleteAllByBrandId method loads all products via
productReader.findAllByBrandId and calls Product.delete() inside one transaction
which will OOM and create massive UPDATEs for large brands; change this to a
chunked or bulk approach: either implement a repository-level bulk soft-delete
like productRepository.bulkSoftDeleteByBrandId(brandId) that issues a single
UPDATE setting deleted flag/timestamp, or iterate using a paged reader (e.g.,
productReader.findAllByBrandIdPaged or a streaming query) and process in
fixed-size chunks, calling delete on each entity then flush() and clear()
between chunks to bound the persistence context and shorten transaction time;
update deleteAllByBrandId to use that new bulk or chunked method and ensure
`@Transactional` boundaries are applied per-chunk if using chunked processing.
apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberReaderImpl.java-24-27 (1)

24-27: ⚠️ Potential issue | 🟠 Major

existsByLoginId에서 soft-delete 필터가 누락되어 있다.

findByLoginId, findById, findAll 메서드는 모두 deletedAt.isNull() 조건을 적용하는데, existsByLoginId는 삭제된 회원도 포함하여 조회한다. MemberService의 register 메서드에서 이 메서드를 사용하여 중복 가입을 방지하므로, 삭제된 회원의 loginId로 신규 가입이 차단되는 운영 이슈가 발생한다.

🐛 soft-delete 필터 추가 제안
 `@Override`
 public boolean existsByLoginId(String loginId) {
-    return memberJpaRepository.existsByLoginId(loginId);
+    return memberJpaRepository.findByLoginIdAndDeletedAtIsNull(loginId).isPresent();
 }

MemberJpaRepository에는 이미 existsByLoginId와 별도로 findByLoginIdAndDeletedAtIsNull 메서드가 존재한다. existsByLoginId를 직접 사용하지 말고, 기존의 soft-delete 필터가 적용된 메서드를 활용하거나, 또는 새로운 existsByLoginIdAndDeletedAtIsNull 메서드를 추가하여 사용해야 한다. 삭제된 회원의 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/infrastructure/member/MemberReaderImpl.java`
around lines 24 - 27, existsByLoginId currently ignores soft-delete and blocks
re-registration; update MemberReaderImpl.existsByLoginId to consult the
soft-delete-aware query (use
memberJpaRepository.findByLoginIdAndDeletedAtIsNull(...) and return presence, or
add and call memberJpaRepository.existsByLoginIdAndDeletedAtIsNull(...)) so
deleted members' loginIds don't block new registrations invoked from
MemberService.register; also add an integration test covering re-registration
after soft-delete.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberAdminV1Controller.java-17-25 (1)

17-25: ⚠️ Potential issue | 🟠 Major

페이지네이션 파라미터의 범위 제한이 필요하다.

size 파라미터에 상한이 없어 size=10000 같은 요청이 들어올 경우 메모리 과다 사용 및 DB 부하가 발생할 수 있다. 운영 환경에서 의도치 않은 대량 조회로 인한 장애를 방지해야 한다.

🛡️ size 상한 검증 추가 제안

Facade 또는 Service 레이어에서 검증하거나, Controller에서 값 보정을 수행한다:

 `@GetMapping`
 public ApiResponse<MemberAdminV1Dto.MemberListResponse> getMembers(
     `@RequestParam`(required = false) String keyword,
     `@RequestParam`(defaultValue = "0") int page,
     `@RequestParam`(defaultValue = "20") int size
 ) {
+    int validatedSize = Math.min(size, 100); // 최대 100건 제한
-    PagedInfo<AdminMemberInfo> result = memberFacade.getMembersForAdmin(keyword, page, size);
+    PagedInfo<AdminMemberInfo> result = memberFacade.getMembersForAdmin(keyword, page, validatedSize);
     return ApiResponse.success(MemberAdminV1Dto.MemberListResponse.from(result));
 }

또는 도메인 레이어에서 검증하여 CoreException을 throw한다.

🤖 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/member/MemberAdminV1Controller.java`
around lines 17 - 25, Add a hard upper bound check for the pagination parameters
in MemberAdminV1Controller.getMembers: introduce a MAX_PAGE_SIZE constant (e.g.,
100) and clamp or validate the incoming size (and ensure page >= 0) before
calling memberFacade.getMembersForAdmin; either adjust size = Math.min(size,
MAX_PAGE_SIZE) or return a 4xx ApiResponse when size > MAX_PAGE_SIZE, and
document/centralize the check so memberFacade.getMembersForAdmin always receives
a safe size value.
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemReader.java-5-8 (1)

5-8: ⚠️ Potential issue | 🟠 Major

findAllByOrderIds 메서드에서 빈 리스트 전달 시 불필요한 데이터베이스 쿼리 방지 필요.

OrderItemReaderImpl.findAllByOrderIds 구현체에서 파라미터로 받은 orderIds가 빈 리스트일 때 즉시 공 리스트를 반환하지 않고 저장소 메서드를 호출한다. 실제로 OrderFacade.toPagedSummary에서 조회 결과가 없을 때 빈 orderIds 리스트를 전달하므로, SQL WHERE id IN () 절을 포함한 불필요한 쿼리가 실행되어 데이터베이스 리소스를 낭비한다.

구현 코드
`@Override`
public List<OrderItem> findAllByOrderIds(List<Long> orderIds) {
    return orderItemJpaRepository.findAllByOrderIdInAndDeletedAtIsNull(orderIds);
}

orderIds.isEmpty() 체크를 추가하여 빈 리스트인 경우 저장소 호출 없이 공 컬렉션을 반환하도록 수정하고, 이 동작을 단위 테스트로 검증한다.

🤖 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/OrderItemReader.java`
around lines 5 - 8, The implementation of OrderItemReader.findAllByOrderIds (in
OrderItemReaderImpl.findAllByOrderIds) should short-circuit when orderIds is
empty to avoid invoking
orderItemJpaRepository.findAllByOrderIdInAndDeletedAtIsNull with an empty
collection; update the method to return Collections.emptyList() (or equivalent)
when orderIds.isEmpty() and otherwise call the repository, and add a unit test
verifying that passing an empty orderIds list does not call the repository and
returns an empty list.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java-23-34 (1)

23-34: ⚠️ Potential issue | 🟠 Major

ProductFacade의 sort 파라미터 검증 누락으로 인한 500 에러 발생

ProductFacade.getProducts() 38-39줄에서 유효하지 않은 sort 값(예: "INVALID")이 전달되면 ProductSortType.valueOf(sort)에서 IllegalArgumentException이 발생한다. 현재는 ApiControllerAdvice의 generic Throwable 핸들러에 의해 500 INTERNAL_ERROR로 응답되며, 사용자에게 유효한 sort 값이 무엇인지 알려주지 않는다.

개선 방안:
ProductFacade의 getProducts() 메서드에서 valueOf() 호출 시 예외를 처리하고, 유효한 sort 타입(LATEST, PRICE_ASC, PRICE_DESC, LIKES_DESC)을 명시한 에러 메시지와 함께 CoreException을 던져야 한다. 이를 통해 사용자는 400 BAD_REQUEST와 함께 올바른 sort 값을 인식할 수 있다.

public PagedInfo<ProductInfo> getProducts(String keyword, Long brandId, String sort, int page, int size) {
    ProductSortType sortType;
    try {
        sortType = ProductSortType.valueOf(sort);
    } catch (IllegalArgumentException e) {
        throw new CoreException(ErrorType.BAD_REQUEST, "유효하지 않은 sort 값입니다. 가능한 값: LATEST, PRICE_ASC, PRICE_DESC, LIKES_DESC");
    }
    // ... 나머지 로직
}
🤖 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 23 - 34, ProductFacade.getProducts currently passes the raw sort
string into ProductSortType.valueOf(...) which throws IllegalArgumentException
for invalid values; wrap the valueOf call in a try/catch inside getProducts,
catch IllegalArgumentException and throw a CoreException with
ErrorType.BAD_REQUEST and a clear message listing valid sort types (e.g., "유효하지
않은 sort 값입니다. 가능한 값: LATEST, PRICE_ASC, PRICE_DESC, LIKES_DESC") so invalid
requests return 400 instead of 500.
apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java-15-15 (1)

15-15: ⚠️ Potential issue | 🟠 Major

findAllByIdInAndDeletedAtIsNull에 IN 절 파라미터 크기 제한에 대한 보호 로직을 추가해야 한다.

BrandReaderImpl의 findAllByIds(List ids) 메서드는 호출자의 ID 리스트 크기를 검증하지 않고 데이터베이스 저장소에 직접 전달한다. 데이터베이스별 IN 절 파라미터 제한(예: Oracle 1000개, SQL Server 1000개)에 도달하면 쿼리 실패 또는 성능 저하가 발생한다.

BrandReaderImpl의 findAllByIds 메서드에 배치 크기 제한(권장 500~1000)을 정의하고, 이를 초과하는 경우 배치 분할 조회를 구현해야 한다. 이때 각 배치 결과를 병합하여 반환해야 한다. 또한 일반적인 사용 사례 이상으로 대량의 ID(5000개 이상)를 전달하는 테스트 케이스를 추가하여 배치 처리 로직의 정확성을 검증해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java`
at line 15, BrandReaderImpl.findAllByIds currently forwards the full id list to
BrandJpaRepository.findAllByIdInAndDeletedAtIsNull which risks DB IN-clause
limits; implement batching (e.g., BATCH_SIZE = 500 or 1000) inside
BrandReaderImpl.findAllByIds to split the incoming List<Long> ids into sublists,
invoke repository.findAllByIdInAndDeletedAtIsNull for each batch, collect and
merge results preserving uniqueness, and return the combined list; add a
unit/integration test that supplies a very large id list (>=5000 ids) to verify
batching and correct merged results.
apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeV1ApiE2ETest.java-261-272 (1)

261-272: ⚠️ Potential issue | 🟠 Major

좋아요 목록 검증이 개수 중심이라 데이터 정확성·격리 회귀를 놓칠 수 있다.

Line 269272, Line 338341은 건수만 검증해 잘못된 항목이 섞여도 통과할 수 있다. 운영 관점에서 타 사용자 데이터 노출 같은 프라이버시 장애를 늦게 발견하게 된다. 반환 ID를 정확히 단정하고, 좋아요하지 않은 항목이 제외되는지까지 검증하도록 강화해야 한다. 추가 테스트로 다른 회원의 좋아요 데이터를 함께 생성한 뒤 내 목록에 포함되지 않는지 검증해야 한다.

🛠 수정 예시
             assertAll(
                 () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK),
-                () -> assertThat(response.getBody().data().products()).hasSize(2)
+                () -> assertThat(response.getBody().data().products()).hasSize(2),
+                () -> assertThat(response.getBody().data().products().stream()
+                    .map(p -> p.product().id())
+                    .toList())
+                    .containsExactlyInAnyOrder(product1, product2)
             );

As per coding guidelines "통합 테스트는 격리 수준, 플래키 가능성, 테스트 데이터 준비/정리 전략을 점검한다."

Also applies to: 330-341

🤖 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/LikeV1ApiE2ETest.java`
around lines 261 - 272, The current assertions in LikeV1ApiE2ETest only check
the product count, which can miss wrong items; update the test that calls the
GET "/api/v1/members/me/likes/products" (in LikeV1ApiE2ETest) to assert exact
product IDs and their order (or unordered set) returned by
response.getBody().data().products(), and add setup that creates likes for
another member and assert those other-member liked IDs are not present in the
response; specifically, replace/augment the hasSize(2) checks with explicit
assertions on the product ID values (and optionally other key fields) and add a
separate scenario that creates a second member with likes to ensure
isolation/exclusion.
apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandReaderImpl.java-41-43 (1)

41-43: ⚠️ Potential issue | 🟠 Major

페이지 파라미터 방어 검증이 없어 비정상 요청이 일관되지 않게 실패할 수 있다.

Line 41~43에서 page, size를 그대로 사용하면 잘못된 값이 전달될 때 런타임 예외가 상위로 전파되어 응답 포맷이 깨질 수 있다. 운영 관점에서 에러 표준화가 깨지면 모니터링/장애 분류가 어려워진다. page >= 0, size > 0을 선검증하고 CoreException(BAD_REQUEST)로 변환하도록 수정해야 한다. 추가 테스트로 page=-1, size=0 요청이 400과 표준 에러 바디를 반환하는지 검증해야 한다.

🛠 수정 예시
+import com.loopers.support.error.CoreException;
+import com.loopers.support.error.ErrorType;
...
     `@Override`
     public PageResult<Brand> findAll(String keyword, int page, int size) {
+        if (page < 0 || size <= 0) {
+            throw new CoreException(ErrorType.BAD_REQUEST, "page는 0 이상, size는 1 이상이어야 합니다.");
+        }
         Pageable pageable = PageRequest.of(page, size);

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/infrastructure/brand/BrandReaderImpl.java`
around lines 41 - 43, In BrandReaderImpl.findAll, add defensive validation to
ensure page >= 0 and size > 0 and if not throw a CoreException with BAD_REQUEST
so the error is handled by ApiControllerAdvice; update the method to check the
incoming page and size before constructing PageRequest and throw new
CoreException(BAD_REQUEST) on invalid values, and add tests asserting that
requests with page=-1 and size=0 return HTTP 400 with the standard error body.
apps/commerce-api/src/test/java/com/loopers/interfaces/api/AddressV1ApiE2ETest.java-235-242 (1)

235-242: ⚠️ Potential issue | 🟠 Major

목록 0번 인덱스 단정은 정렬 정책 변경 시 플래키를 유발한다.

Line 241에서 첫 번째 원소를 바로 단정하면 정렬 기준 변경이나 DB 반환 순서 변화 시 테스트가 간헐적으로 실패한다. 운영 관점에서 CI 불안정은 실제 장애 신호를 희석시킨다. 수정 대상 주소를 addressId로 찾아 검증하도록 바꾸는 편이 안전하다. 추가 테스트로 주소 2개를 둔 상태에서 두 번째 주소를 수정해도 ID 기준 검증이 안정적으로 통과하는지 확인해야 한다.

🛠 수정 예시
-            assertThat(listResponse.getBody().data().addresses().get(0).label()).isEqualTo("새집");
+            var updated = listResponse.getBody().data().addresses().stream()
+                .filter(a -> a.id().equals(addressId))
+                .findFirst();
+            assertThat(updated).isPresent();
+            assertThat(updated.get().label()).isEqualTo("새집");

As per coding guidelines "통합 테스트는 격리 수준, 플래키 가능성, 테스트 데이터 준비/정리 전략을 점검한다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/test/java/com/loopers/interfaces/api/AddressV1ApiE2ETest.java`
around lines 235 - 242, The assertion that directly checks the first list
element (listResponse.getBody().data().addresses().get(0).label()) is brittle;
update the test in AddressV1ApiE2ETest to locate the modified address by its
addressId instead of by index (use the response list returned from
ENDPOINT_ADDRESSES to find the DTO whose id matches the expected addressId and
assert its label), and add a second scenario where two addresses are created,
then modify the second address and verify changes by searching the list for that
addressId to ensure the test is stable against ordering changes.
apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java-82-87 (1)

82-87: ⚠️ Potential issue | 🟠 Major

삭제된 상품에 대한 주문 취소 시 예외가 발생하여 취소 자체가 실패할 수 있다.

Line 84에서 productService.getProduct(item.getProductId())를 호출하는데, 주문 이후 해당 상품이 삭제되었다면 NOT_FOUND 예외가 발생한다. 이 경우:

  1. 주문 취소 트랜잭션 전체가 롤백됨
  2. 고객은 삭제된 상품이 포함된 주문을 취소할 수 없음

운영 관점에서 상품 삭제와 주문 취소 간의 의존성으로 인해 CS 이슈가 발생할 수 있다.

🔧 권장 수정: 삭제된 상품 조회 허용 또는 예외 처리

방법 1: 삭제된 상품 포함 조회 메서드 추가

// ProductReader
Optional<Product> findByIdIncludingDeleted(Long id);

방법 2: 예외 처리로 스킵

for (OrderItem item : items) {
    try {
        Product product = productService.getProduct(item.getProductId());
        product.increaseStock(item.getQuantity());
    } catch (CoreException e) {
        if (e.getErrorType() == ErrorType.NOT_FOUND) {
            log.warn("삭제된 상품 재고 복원 스킵: productId={}", item.getProductId());
            continue;
        }
        throw e;
    }
}

삭제된 상품 포함 주문 취소 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/application/order/OrderFacade.java`
around lines 82 - 87, In the OrderFacade stock restore loop that calls
productService.getProduct(item.getProductId()) for each OrderItem, handle the
case where the product was deleted so cancellation doesn't fail: either update
productService/ProductReader to expose a findByIdIncludingDeleted(Long) and use
it when restoring stock, or catch the CoreException from getProduct and if
e.getErrorType() == ErrorType.NOT_FOUND log a warning (including
item.getProductId()) and continue; ensure you still rethrow unexpected
exceptions and add an E2E test for cancelling an order containing a deleted
product.
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java-39-41 (1)

39-41: ⚠️ Potential issue | 🟠 Major

likeCount 증감 시 동시성 경합(lost update)이 발생할 수 있다.

toggleLikeincreaseLikeCount/decreaseLikeCount를 호출하는 과정에서, 두 사용자가 동시에 같은 상품에 좋아요를 누르면 getBrand/getProductlikeCount 읽기 → 증가 → 저장 사이에 경합이 발생하여 count가 누락될 수 있다.

운영 환경에서 인기 상품에 대한 동시 좋아요 시 likeCount 정확도가 저하된다. 즉각적인 장애는 아니나, 데이터 정합성 관점에서 개선이 필요하다.

🔧 권장 수정: 비관적 잠금 또는 UPDATE 쿼리 직접 실행

방법 1: @Lock(LockModeType.PESSIMISTIC_WRITE) 적용

// BrandReader/ProductReader에 잠금 조회 메서드 추가
Optional<Brand> findByIdForUpdate(Long id);

방법 2: UPDATE 쿼리 직접 실행 (권장)

// BrandRepository에 추가
`@Modifying`
`@Query`("UPDATE Brand b SET b.likeCount = b.likeCount + 1 WHERE b.id = :id")
int incrementLikeCount(`@Param`("id") Long id);

동시성 테스트 추가를 권장한다:

`@Test`
void concurrentLikeToggle_shouldMaintainCorrectCount() throws Exception {
    // 다중 스레드로 동시 좋아요 토글 후 최종 count 검증
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java`
around lines 39 - 41, The likeCount update is vulnerable to lost-update race
conditions when toggleLike calls
productService.increaseLikeCount/decreaseLikeCount concurrently; fix by using a
database-level update or pessimistic lock instead of read-modify-write. Replace
the current read-then-save logic in increaseLikeCount and decreaseLikeCount with
a single atomic UPDATE statement (add e.g. incrementLikeCount/decrementLikeCount
`@Modifying` queries to BrandRepository/ProductRepository and call them from
ProductService) or add a findByIdForUpdate method on BrandReader/ProductReader
and annotate with `@Lock`(LockModeType.PESSIMISTIC_WRITE) to lock the row inside
toggleLike; also add a concurrent unit test
(concurrentLikeToggle_shouldMaintainCorrectCount) to verify correctness under
multithreaded toggles.
apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java-85-96 (1)

85-96: ⚠️ Potential issue | 🟠 Major

cancelOrder 메서드에 @Transactional 어노테이션이 누락되었다.

order.cancel() 호출로 엔티티 상태가 변경되지만, 이 메서드에는 @Transactional이 없다. 현재 OrderFacade에서 트랜잭션 컨텍스트 내에서 호출되어 동작하지만, 이 서비스 메서드가 다른 곳에서 직접 호출될 경우 상태 변경이 영속화되지 않는 장애가 발생할 수 있다.

운영 관점에서 트랜잭션 경계가 명확하지 않으면 추후 유지보수 시 예상치 못한 데이터 불일치 문제를 유발할 수 있다.

🔧 수정안
+    `@Transactional`
     public List<OrderItem> cancelOrder(Long orderId, Long memberId) {
         Order order = orderReader.findByIdAndMemberId(orderId, memberId)
             .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "주문을 찾을 수 없습니다."));

추가 테스트: cancelOrder를 Facade가 아닌 Service 단독 호출 시에도 DB 상태 변경이 정상 반영되는지 확인하는 통합 테스트를 권장한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java`
around lines 85 - 96, The cancelOrder method in OrderService mutates the Order
entity via order.cancel() but lacks a transactional boundary; annotate the
method (or the OrderService class) with `@Transactional` so the entity state
changes are flushed and persisted when this service is called directly, ensure
the proper import of org.springframework.transaction.annotation.Transactional,
and keep existing behavior when called from OrderFacade; add an integration test
that calls OrderService.cancelOrder directly to verify the DB state is updated.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a442e0 and c688447.

⛔ Files ignored due to path filters (5)
  • .claude/skills/requirements-analysis/SKILL.md is excluded by !**/*.md and included by **
  • docs/design/01-requirements.md is excluded by !**/*.md and included by **
  • docs/design/02-sequence-diagrams.md is excluded by !**/*.md and included by **
  • docs/design/03-class-diagram.md is excluded by !**/*.md and included by **
  • docs/design/04-erd.md is excluded by !**/*.md and included by **
📒 Files selected for processing (124)
  • apps/commerce-api/build.gradle.kts
  • apps/commerce-api/src/main/java/com/loopers/application/PagedInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/address/AddressFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/address/AddressInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/BrandLikeInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/LikeToggleInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/like/ProductLikeInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/member/AdminMemberInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/member/MemberFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/member/MemberInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderSummaryInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java
  • apps/commerce-api/src/main/java/com/loopers/domain/PageResult.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/Address.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/AddressReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/AddressRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/address/AddressService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/like/LikeTargetType.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/Gender.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/Member.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/MemberReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/MemberRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/member/PasswordEncoder.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItemRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductReader.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java
  • apps/commerce-api/src/main/java/com/loopers/domain/product/ProductSortType.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/address/AddressJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/address/AddressReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/address/AddressRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/member/MemberRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/member/PasswordEncoderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderItemRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductReaderImpl.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/address/AddressV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/address/AddressV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/brand/BrandV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberAdminV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductAdminV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-api/src/main/java/com/loopers/support/auth/AdminAuthFilter.java
  • apps/commerce-api/src/main/java/com/loopers/support/auth/AuthUtils.java
  • apps/commerce-api/src/main/java/com/loopers/support/auth/MemberAuthFilter.java
  • apps/commerce-api/src/main/java/com/loopers/support/config/SecurityConfig.java
  • apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
  • apps/commerce-api/src/test/java/com/loopers/ArchitectureTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/address/AddressServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/address/AddressTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/like/LikeTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/member/MemberServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/member/MemberTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderItemTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/order/OrderTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceTest.java
  • apps/commerce-api/src/test/java/com/loopers/domain/product/ProductTest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/AddressV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/BrandV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/LikeV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/MemberAdminV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/MemberV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/OrderV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ProductV1ApiE2ETest.java
  • apps/commerce-api/src/test/java/com/loopers/support/auth/AdminAuthFilterE2ETest.java
  • http/commerce-api/address-v1.http
  • http/commerce-api/brand-v1.http
  • http/commerce-api/like-v1.http
  • http/commerce-api/member-v1.http
  • http/commerce-api/order-v1.http
  • http/commerce-api/product-v1.http

Comment on lines +53 to +61
Product product = productService.getProduct(productId);
product.validateOrderQuantity(quantity);
product.decreaseStock(quantity);

totalAmount += product.getPrice() * quantity;
commands.add(new OrderService.OrderItemCommand(
productId, product.getName(), product.getPrice(), quantity
));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and examine OrderFacade.java to see the full context of createOrder method
fd -t f "OrderFacade.java" apps/commerce-api/src/main/java/

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 163


🏁 Script executed:

# Check the OrderFacade.createOrder method with `@Transactional` annotation
rg -n "class OrderFacade|@Transactional|void createOrder|public.*createOrder" -A5 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java | head -50

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2373


🏁 Script executed:

# Check ProductService.getProduct `@Transactional` setting
fd -t f "ProductService.java" apps/commerce-api/src/main/java/ && cat apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java | head -100

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2733


🏁 Script executed:

# Search for decreaseStock and increaseStock usage patterns to verify if save() is called
rg -n "decreaseStock|increaseStock" -B2 -A5 apps/commerce-api/src/main/java/

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 3609


🏁 Script executed:

# Get the full createOrder method in OrderFacade
sed -n '32,74p' apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1805


🏁 Script executed:

# Also check the cancelOrder method that uses increaseStock
sed -n '75,87p' apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 568


🏁 Script executed:

# Check if ProductRepository has any explicit save calls
rg -n "productRepository.save" apps/commerce-api/src/main/java/

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 214


readOnly 트랜잭션에서 엔티티 상태 변경이 DB에 반영되지 않는다.

ProductService.getProduct()@Transactional(readOnly = true) 설정으로 인해 이 메서드 내에서 반환된 Product 엔티티에 대한 수정사항이 데이터베이스에 반영되지 않는다. 54-55줄에서 product.validateOrderQuantity(quantity)product.decreaseStock(quantity)를 호출하지만, readOnly 트랜잭션 컨텍스트에서는 Hibernate 세션이 읽기 전용 모드로 설정되어 dirty checking을 통한 변경사항 반영이 차단된다.

운영 관점의 문제점:

  • 주문 생성 후에도 실제 재고가 차감되지 않아 재고 불일치 발생
  • 동시성 주문 시 재고 초과 판매(overselling) 위험
  • 주문 취소(cancelOrder) 시 increaseStock() 호출도 동일한 문제 발생

수정안:
ProductService에 decreaseStock()increaseStock() 전용 메서드를 추가하여 @Transactional (readOnly=false)로 선언하거나, OrderFacade에서 productService를 통해 재고를 수정하도록 구조 변경이 필요하다.

테스트 방법:
createOrder() 호출 후 직접 데이터베이스 조회하여 Product 테이블의 stock_quantity 컬럼이 실제로 감소했는지 검증한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java`
around lines 53 - 61, The Product entity mutations performed in OrderFacade
(calls to product.validateOrderQuantity(...) and product.decreaseStock(...)) are
happening on an entity obtained via ProductService.getProduct() which is
annotated `@Transactional`(readOnly = true), so changes are not flushed; fix by
adding write-side methods on ProductService such as decreaseStock(productId,
quantity) and increaseStock(productId, quantity) annotated `@Transactional` (no
readOnly) and move the stock-change logic into those methods (or call those new
methods from OrderFacade instead of mutating the returned Product), and update
cancelOrder to call ProductService.increaseStock(...) similarly so DB updates
are persisted via a write transaction.

Comment on lines +21 to +30
public Address register(Long memberId, String label, String recipientName, String recipientPhone,
String zipCode, String address1, String address2) {
long count = addressReader.countByMemberId(memberId);
if (count >= MAX_ADDRESS_COUNT) {
throw new CoreException(ErrorType.BAD_REQUEST, "배송지는 최대 " + MAX_ADDRESS_COUNT + "개까지 등록할 수 있습니다.");
}
boolean isDefault = count == 0;
Address address = Address.create(memberId, label, recipientName, recipientPhone,
zipCode, address1, address2, isDefault);
return addressRepository.save(address);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

동시 등록 시 주소 수 상한과 기본 배송지 단일성이 깨질 수 있다.

운영 관점에서 동일 회원의 등록 요청이 동시에 들어오면 count 기반 검증이 경합에 취약해 10개 초과 저장 또는 기본 배송지 2건 이상이 발생할 수 있어 데이터 정합성이 깨진다.
수정안으로 회원 단위 잠금(FOR UPDATE 기반 조회 또는 멤버 행 잠금)으로 등록을 직렬화하고, DB에 deleted_at IS NULL AND is_default = true 조건의 유니크 인덱스를 추가해 최종 방어선을 두는 것이 필요하다.
추가 테스트로 동일 회원에 대해 동시 등록 요청을 발생시켜 총 주소 수가 10개를 넘지 않고 기본 배송지가 정확히 1개인지 검증해야 한다.

수정 예시
 `@Transactional`
 public Address register(Long memberId, String label, String recipientName, String recipientPhone,
                         String zipCode, String address1, String address2) {
-    long count = addressReader.countByMemberId(memberId);
+    List<Address> addresses = addressReader.findAllByMemberIdForUpdate(memberId);
+    long count = addresses.size();
     if (count >= MAX_ADDRESS_COUNT) {
         throw new CoreException(ErrorType.BAD_REQUEST, "배송지는 최대 " + MAX_ADDRESS_COUNT + "개까지 등록할 수 있습니다.");
     }
-    boolean isDefault = count == 0;
+    boolean isDefault = addresses.stream().noneMatch(Address::getIsDefault);
     Address address = Address.create(memberId, label, recipientName, recipientPhone,
         zipCode, address1, address2, isDefault);
     return addressRepository.save(address);
 }
CREATE UNIQUE INDEX ux_address_member_default_active
ON address(member_id)
WHERE deleted_at IS NULL AND is_default = true;

As per coding guidelines **/*Service*.java: 트랜잭션 경계와 멱등성/중복 처리 방지 전략을 점검해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/address/AddressService.java`
around lines 21 - 30, The current AddressService.register flow uses
addressReader.countByMemberId then creates and saves an Address, which is racy
under concurrent requests and can allow >MAX_ADDRESS_COUNT or multiple
is_default=true rows; fix by moving register into a transactional boundary
(annotate AddressService.register or calling method) and acquire a member-scoped
lock when checking addresses (e.g., SELECT ... FOR UPDATE on the member row or
use a repository method that locks existing addresses for the member before
counting), enforce the MAX_ADDRESS_COUNT check inside that transaction, ensure
only one address is marked default by clearing is_default on other addresses for
the member within the same transaction before saving, and add a DB-level unique
partial index (e.g., on member_id WHERE deleted_at IS NULL AND is_default =
true) as a final defensive constraint to prevent dual defaults; reference
AddressService.register, addressReader.countByMemberId, addressRepository.save,
and Address.create when applying these changes and add concurrent-insertion
tests to validate count and single-default invariants.

Comment on lines +58 to +67
public void changeDefault(Long id, Long memberId) {
Address newDefault = getAddress(id, memberId);
List<Address> addresses = addressReader.findAllByMemberId(memberId);
for (Address address : addresses) {
if (address.getIsDefault()) {
address.changeDefault(false);
}
}
newDefault.changeDefault(true);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

기본 배송지 변경 로직이 동시성 상황에서 복수 기본 배송지를 남길 수 있다.

운영 관점에서 두 요청이 동시에 다른 주소를 기본으로 변경하면, 각 트랜잭션이 자신이 본 스냅샷 기준으로만 isDefault를 내리기 때문에 최종적으로 두 주소가 모두 기본 상태로 남을 수 있다.
수정안으로 회원 주소 집합을 잠금 조회한 뒤 일괄적으로 기본값을 해제하고 대상 1건만 기본값으로 설정하거나, 벌크 업데이트 + 유니크 인덱스로 단일성을 강제해야 한다.
추가 테스트로 서로 다른 주소를 대상으로 동시 changeDefault를 실행해 항상 기본 배송지가 정확히 1건인지 검증해야 한다.

수정 예시
 `@Transactional`
 public void changeDefault(Long id, Long memberId) {
-    Address newDefault = getAddress(id, memberId);
-    List<Address> addresses = addressReader.findAllByMemberId(memberId);
-    for (Address address : addresses) {
-        if (address.getIsDefault()) {
-            address.changeDefault(false);
-        }
-    }
-    newDefault.changeDefault(true);
+    List<Address> addresses = addressReader.findAllByMemberIdForUpdate(memberId);
+    Address newDefault = addresses.stream()
+        .filter(address -> address.getId().equals(id))
+        .findFirst()
+        .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "배송지를 찾을 수 없습니다."));
+
+    for (Address address : addresses) {
+        address.changeDefault(false);
+    }
+    newDefault.changeDefault(true);
 }

As per coding guidelines **/*Service*.java: 트랜잭션 경계/전파와 부수 효과를 명확히 하고 중복 처리 방지 전략을 점검해야 한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/address/AddressService.java`
around lines 58 - 67, The changeDefault method can leave multiple addresses
marked default under concurrent requests; wrap the operation in a transaction
and perform a locked/bulk update: fetch the member's addresses with a
pessimistic lock (or execute a single bulk UPDATE clearing isDefault for
memberId via addressReader/repository), then set the target Address (from
getAddress(id, memberId)) to isDefault=true using Address.changeDefault(true)
and save; alternatively also enforce a database unique partial index on
(member_id) where is_default=true to guarantee uniqueness and add a concurrency
test that runs concurrent changeDefault calls on different ids to assert exactly
one default remains.

Comment on lines +19 to +27
Optional<Like> existing = likeReader.findByMemberIdAndTargetTypeAndTargetId(memberId, targetType, targetId);

if (existing.isPresent()) {
likeRepository.delete(existing.get());
return false;
}

Like like = Like.create(memberId, targetType, targetId);
likeRepository.save(like);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the LikeService.java file
find . -type f -name "LikeService.java" -path "*/domain/like/*" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 159


🏁 Script executed:

# Also search for Like entity and related files
find . -type f -name "Like*.java" | grep -E "(domain|entity)" | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 608


🏁 Script executed:

# Look for database schema or JPA configuration
find . -type f \( -name "*.sql" -o -name "schema*" \) | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Read the LikeService.java file
cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1455


🏁 Script executed:

# Read the Like entity
cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 3025


🏁 Script executed:

# Read LikeRepository to understand persistence
cat -n ./apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 250


🏁 Script executed:

# Check LikeServiceTest for concurrency tests
cat -n ./apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 6382


🏁 Script executed:

# Check if there's a Spring Boot configuration file for transaction isolation level
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 256


🏁 Script executed:

# Look for any JPA/Hibernate configuration that might affect isolation levels
rg -l "hibernate|jpa|transaction" --type properties --type yaml | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 119


🏁 Script executed:

# Check if there are any integration tests that might test with actual DB
find . -path "*/test/*" -name "*IntegrationTest*.java" -o -path "*/test/*" -name "*ConcurrencyTest*.java" | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 180


🏁 Script executed:

# Check the main application.yml for transaction/JPA configuration
cat -n ./apps/commerce-api/src/main/resources/application.yml

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 1450


🏁 Script executed:

# Look at the integration test pattern to understand testing approach
cat -n ./apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2972


🏁 Script executed:

# Check if there's any global exception handling or DataIntegrityViolationException handling
rg "DataIntegrityViolationException|ConstraintViolation|UniqueConstraint" --type java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 315


🏁 Script executed:

# Check the jpa.yml configuration for isolation level details
cat -n ./apps/commerce-api/src/main/resources/jpa.yml

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 163


🏁 Script executed:

# Check for any global exception handlers
find ./apps/commerce-api -type f -name "*ExceptionHandler*.java" -o -name "*ErrorHandler*.java"

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Look for any use of pessimistic locking or SELECT FOR UPDATE patterns in the codebase
rg "LockModeType|SELECT FOR UPDATE|@Lock" --type java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Find jpa.yml file
find . -name "jpa.yml" -type f

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 125


🏁 Script executed:

# Look for exception handlers in infrastructure layer
find ./apps/commerce-api -type f -name "*.java" | xargs grep -l "ControllerAdvice\|ExceptionHandler" | head -10

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 170


🏁 Script executed:

# Check if there's any retry mechanism or circuit breaker for repository calls
rg "@Retryable|@CircuitBreaker|Resilience4j" --type java | head -20

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Read the jpa.yml configuration
cat -n ./modules/jpa/src/main/resources/jpa.yml

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2903


🏁 Script executed:

# Check the ApiControllerAdvice to see if DataIntegrityViolationException is handled
cat -n ./apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 6820


동시 요청 시 중복 삽입 유니크 제약 예외가 500으로 전파된다

라인 19-27의 check-then-act 패턴으로 인해 동일한 사용자의 빠른 연타나 재시도 요청 시 두 트랜잭션 모두 existing이 없다고 판단한 뒤 insert 경쟁 조건이 발생할 수 있다. 두 번째 insert는 uk_like_member_target 유니크 제약을 위반하여 DataIntegrityViolationException을 발생시키는데, 현재 ApiControllerAdvice에서 이를 처리하지 않으므로 500 오류로 전파된다. 운영 관점에서 정상적인 사용자 동작(더블 클릭, 재시도)으로 간헐 500이 발생하면 장애로 인지된다.

저장소 단에서 원자적 토글로 개선하거나, 최소한 중복 제약 예외를 도메인 규칙에 맞게 흡수하여 일관된 응답을 반환해야 한다. 또한 동일한 (memberId, targetType, targetId)에 대해 동시 토글 부하를 걸어 500 미발생과 최종 상태 일관성을 검증하는 동시성 통합 테스트를 추가하는 것이 필요하다.

⚙️ 임시 완화 예시
+import org.springframework.dao.DataIntegrityViolationException;
@@
     public boolean toggleLike(Long memberId, LikeTargetType targetType, Long targetId) {
         Optional<Like> existing = likeReader.findByMemberIdAndTargetTypeAndTargetId(memberId, targetType, targetId);

         if (existing.isPresent()) {
             likeRepository.delete(existing.get());
             return false;
         }

-        Like like = Like.create(memberId, targetType, targetId);
-        likeRepository.save(like);
-        return true;
+        try {
+            Like like = Like.create(memberId, targetType, targetId);
+            likeRepository.save(like);
+            return true;
+        } catch (DataIntegrityViolationException e) {
+            Optional<Like> concurrent = likeReader.findByMemberIdAndTargetTypeAndTargetId(memberId, targetType, targetId);
+            if (concurrent.isPresent()) {
+                likeRepository.delete(concurrent.get());
+                return false;
+            }
+            throw e;
+        }
     }
🤖 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/LikeService.java`
around lines 19 - 27, The check-then-act race causes
DataIntegrityViolationException on concurrent inserts; update LikeService to
handle this by (1) replacing the fragile pattern with an atomic repository
toggle (preferred) or at minimum wrap the likeRepository.save call in a
try/catch for DataIntegrityViolationException (or the specific constraint
violation), and on catch treat it as "already exists" and return the same
logical result as a successful insert (i.e., return true) instead of letting the
exception propagate to ApiControllerAdvice; reference
likeReader.findByMemberIdAndTargetTypeAndTargetId, likeRepository.save,
likeRepository.delete, Like.create and the uk_like_member_target unique
constraint, and add a concurrency integration test that simulates parallel
toggles for the same (memberId, targetType, targetId) to assert no 500s and
consistent final state.

Comment on lines +69 to +74
public void decreaseStock(int quantity) {
if (this.stockQuantity < quantity) {
throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다.");
}
this.stockQuantity -= quantity;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

음수/0 수량 입력이 재고 무결성을 깨뜨릴 수 있다.

운영 관점에서 decreaseStock(-1)은 재고를 감소가 아니라 증가시키며, validateOrderQuantity가 0/음수를 허용해 주문 경로에서 비정상 수량이 통과될 수 있어 재고 데이터 오염과 악용 가능성이 생긴다.
수정안으로 차감/주문 수량 모두 quantity > 0을 강제하고, 예외는 CoreException으로 일관 처리해야 한다.
추가 테스트로 decreaseStock(-1), decreaseStock(0), validateOrderQuantity(-1/0)가 모두 예외를 던지고 재고가 변하지 않는지 검증해야 한다.

수정 예시
 public void decreaseStock(int quantity) {
+    validatePositive((long) quantity, "차감 수량은 0보다 커야 합니다.");
     if (this.stockQuantity < quantity) {
         throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다.");
     }
     this.stockQuantity -= quantity;
 }

 public void validateOrderQuantity(int quantity) {
+    if (quantity <= 0) {
+        throw new CoreException(ErrorType.BAD_REQUEST, "주문 수량은 0보다 커야 합니다.");
+    }
     if (quantity > this.maxOrderQuantity) {
         throw new CoreException(ErrorType.BAD_REQUEST,
             "상품 '" + this.name + "'의 최대 주문 수량(" + this.maxOrderQuantity + ")을 초과했습니다.");
     }
 }

As per coding guidelines **/domain/**/*.java: 도메인 규칙을 엔티티에서 일관되게 강제해 불변성과 캡슐화를 보장해야 한다.

Also applies to: 91-95

🤖 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 69 - 74, The decreaseStock method currently allows zero/negative
quantities which can increase stock or leave it unchanged; update
Product.decreaseStock to validate that quantity > 0 and throw a
CoreException(ErrorType.BAD_REQUEST, "...") for non-positive inputs before any
mutation, and keep the existing insufficient-stock check; likewise update/ensure
validateOrderQuantity (the method referenced around lines 91-95) enforces
quantity > 0 and throws CoreException on invalid values so domain invariants are
enforced in the entity; add unit tests asserting decreaseStock(-1),
decreaseStock(0), and validateOrderQuantity(-1/0) all throw CoreException and
that stockQuantity is unchanged after these calls.

Comment on lines +46 to +71
public static ProductLikeListResponse from(PagedInfo<ProductLikeInfo> result) {
var dtos = result.content().stream()
.map(info -> new ProductLikeDto(
new ProductDto(
info.product().id(),
info.product().name(),
info.product().description(),
info.product().price(),
info.product().stockQuantity(),
info.product().maxOrderQuantity(),
info.product().likeCount(),
new BrandDto(
info.product().brand().id(),
info.product().brand().name(),
info.product().brand().description(),
info.product().brand().likeCount()
)
),
info.likedAt()
))
.toList();
return new ProductLikeListResponse(
dtos,
new PageInfo(result.page(), result.size(), result.totalElements(), result.totalPages())
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and explore surrounding code
fd "LikeV1Dto.java" --type f

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 163


🏁 Script executed:

# Check the ProductLikeInfo and BrandLikeInfo definitions
rg -l "class ProductLikeInfo|record ProductLikeInfo" --type java -g '!*Test*'

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 166


🏁 Script executed:

# Check for PagedInfo definition
rg -l "class PagedInfo|record PagedInfo" --type java -g '!*Test*'

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 155


🏁 Script executed:

# Search for ProductLikeInfo usage to understand how it's created
rg "ProductLikeInfo" --type java -g '!*Test*' -B2 -A2 | head -60

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 5679


🏁 Script executed:

# Look for the service/facade layer that creates PagedInfo<ProductLikeInfo>
rg "PagedInfo<ProductLikeInfo>|new PagedInfo" --type java -g '!*Test*' -B3 -A3 | head -80

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 7879


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/like/ProductLikeInfo.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 836


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 5343


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java | sed -n '45,110p'

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 2918


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 567


🏁 Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/product/ProductInfo.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 998


🏁 Script executed:

# Check if there are any existing null checks or defensive patterns in similar code
rg "Objects.requireNonNull|if.*== null|Objects.isNull" --type java apps/commerce-api/src/main/java/com/loopers/application/like/ -B2 -A2

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 67


🏁 Script executed:

# Check BrandLikeInfo structure for comparison
cat -n apps/commerce-api/src/main/java/com/loopers/application/like/BrandLikeInfo.java

Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java

Length of output: 675


LikeFacade.getMyLikedProducts()에서 brandMap.get()이 null을 반환할 수 있어 NPE 발생 위험이 있다.

LikeFacade 61-85번 라인에서 getMyLikedProducts() 메서드가 ProductLikeInfo를 생성할 때, 79번 라인의 Brand brand = brandMap.get(product.getBrandId());는 null을 반환할 수 있다. 이는 brand가 데이터베이스에서 삭제되었거나 존재하지 않는 경우 발생한다. 이 null 값이 80번 라인의 ProductLikeInfo.of(like, product, brand)로 전달되고, ProductLikeInfo 내부에서 BrandInfo.from(brand)을 호출할 때 brand.getId() 등의 메서드 호출로 인해 NPE가 발생한다.

같은 파일의 getMyLikedBrands() 메서드(88-106번 라인)는 98번 라인에서 .filter(like -> brandMap.containsKey(like.getTargetId()))로 brandMap에 존재하는 항목만 필터링하는 방식으로 안전하게 처리하고 있다. getMyLikedProducts() 메서드도 동일한 패턴으로 수정하거나, BrandInfo.from()에 Objects.requireNonNull() 또는 null 체크를 추가하여 운영 시점에 문제를 조기에 감지할 수 있도록 개선하기를 권장한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/commerce-api/src/main/java/com/loopers/interfaces/api/like/LikeV1Dto.java`
around lines 46 - 71, LikeFacade.getMyLikedProducts() can pass a null Brand from
brandMap into ProductLikeInfo, causing NPE later; update the method to filter
out likes whose product brandId is not present in brandMap (use the same pattern
as getMyLikedBrands() with brandMap.containsKey(product.getBrandId())) before
constructing ProductLikeInfo, or alternatively add an explicit null check (e.g.,
Objects.requireNonNull) inside ProductLikeInfo.of or BrandInfo.from to fail fast
with a clear message; locate and change the mapping logic in
LikeFacade.getMyLikedProducts() that builds ProductLikeInfo and ensure
brandMap.get(...) is guarded.

Comment on lines +21 to +23
private static final String HEADER_LDAP = "X-Loopers-Ldap";
private static final String ADMIN_LDAP_VALUE = "loopers.admin";

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

관리자 인증값 하드코딩은 즉시 우회 가능한 인증 취약점이다

Line 22의 고정 문자열은 코드 유출/열람만으로 재현 가능한 값이라 헤더 스푸핑에 취약하고, 운영 중 로테이션도 불가능하다. 운영 관점에서 관리 API 전체가 단일 정적 값에 의존하면 침해 시 피해 반경이 너무 크다. 시크릿 매니저/환경설정으로 외부화하고 주기적 교체가 가능하도록 바꾸는 것이 필요하다. 추가 테스트로 잘못된 값/누락/정상 값/회전된 값 각각에 대해 401/200을 검증하는 통합 테스트를 추가하는 것이 좋다.

🔒 제안 수정안
 import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
@@
-    private static final String ADMIN_LDAP_VALUE = "loopers.admin";
+    `@Value`("${security.admin.ldap-value}")
+    private String adminLdapValue;
@@
-        if (!ADMIN_LDAP_VALUE.equals(ldapValue)) {
+        if (!adminLdapValue.equals(ldapValue)) {
             sendUnauthorizedResponse(response);
             return;
         }

Also applies to: 34-37

🤖 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/support/auth/AdminAuthFilter.java`
around lines 21 - 23, AdminAuthFilter currently hardcodes the admin LDAP header
value (HEADER_LDAP / ADMIN_LDAP_VALUE) which is insecure; remove the hardcoded
ADMIN_LDAP_VALUE and load the expected admin value from a configuration/secret
source (e.g., environment variable or secret manager) at startup in
AdminAuthFilter, ensure the filter fails closed when the configured secret is
missing or invalid (log and return 401), and support rotation by reading the
value from a centrally managed config client or refreshing cache as appropriate;
also add integration tests for AdminAuthFilter that assert 401 on
missing/incorrect header, 200 on the correct header, and a test for rotated
value behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant