[Feat] 상품 목록 조회(정렬), 특가 상품 목록 조회(10개), 비슷한 상품 목록(6개)#14
[Feat] 상품 목록 조회(정렬), 특가 상품 목록 조회(10개), 비슷한 상품 목록(6개)#14kangcheolung merged 6 commits intodevelopfrom
Conversation
…필드 추가, ProductSortType 추가
WalkthroughProduct 엔티티에 Changes
Sequence DiagramsequenceDiagram
actor Client
participant Controller as ProductController
participant Service as ProductService
participant Repository as ProductRepository
participant DB as Database
Client->>Controller: GET /api/products?categoryId=...&sortType=POPULAR&page=...
Controller->>Service: getProducts(condition, sortType, pageable)
Service->>Service: parsePriceRange(), createSort()
Service->>Repository: findAllByCondition(..., pageable)
Repository->>DB: Execute SQL (with EntityGraph)
DB-->>Repository: Product page
Repository-->>Service: Page<Product>
Service->>Service: Convert to ProductSimpleResponse (include reviewCount)
Service-->>Controller: Page<ProductSimpleResponse>
Controller-->>Client: DataResponse(Page<ProductSimpleResponse>)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In @src/main/java/com/ongil/backend/domain/product/entity/Product.java:
- Around line 73-77: You added reviewCount as a non-nullable column and defined
popularity with @Formula, which can break runtime/schema validation if existing
rows have NULL review_count or if view_count/purchase_count are NULL; create a
DB migration that backfills existing rows setting review_count = 0 and adds the
new column with DEFAULT 0 and NOT NULL, and update the @Formula on the
popularity field to be null-safe by using COALESCE for view_count and
purchase_count (e.g., COALESCE(view_count, 0) + COALESCE(purchase_count, 0)) so
popularity never becomes NULL.
In
@src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java:
- Around line 16-34: Update parsePriceRange() to validate order and
non-negativity and to distinguish no-filter from parse errors: when priceRange
is null/empty return Optional.empty() (or a sentinel indicating "no filter"),
otherwise parse trimmed parts into integers, reject if parts length != 2 or
parsing fails by throwing a clear IllegalArgumentException or returning a
distinct Optional.empty() vs Optional.ofError pattern; additionally validate
minPrice >= 0, maxPrice >= 0 and minPrice <= maxPrice before returning new
Integer[] {minPrice, maxPrice} (use the method name parsePriceRange() and its
return type to implement Optional or throw a specific exception so callers can
differentiate "no filter" vs "invalid input").
In @src/main/java/com/ongil/backend/product/service/ProductService.java:
- Around line 138-157: The createSort(ProductSortType sortType) method currently
adds an id tiebreaker only for POPULAR; to ensure stable pagination, update each
case (REVIEW, PRICE_HIGH, PRICE_LOW) to append Sort.Order.asc("id") as the final
tie-breaker (maintain existing primary/secondary orders such as reviewCount,
price, popularity), so Sort.by(...) for all branches includes an id order to
guarantee deterministic DB ordering.
- Around line 54-81: In getProducts, you create pageableWithSort using
createSort(sortType) but then pass the original pageable into
productRepository.findAllByCondition so sorting is ignored; fix by passing
pageableWithSort to productRepository.findAllByCondition (replace the pageable
argument with pageableWithSort) so the Sort from createSort(sortType) is
applied, and ensure variable names pageableWithSort and
productRepository.findAllByCondition in the getProducts method are updated
accordingly.
🧹 Nitpick comments (3)
src/main/resources/application-local.yml (1)
15-15: 스키마 변경 처리 방식을 인지하고 계십시오.
ddl-auto: update로 변경하면 기존 데이터를 보존하면서 스키마를 점진적으로 업데이트합니다. 개발 환경에서는 편리하지만, 컬럼 이름 변경이나 타입 변경 같은 복잡한 마이그레이션은 제대로 처리하지 못할 수 있습니다.로컬 환경이므로 적절한 선택이지만, 프로덕션 배포 시에는 Flyway나 Liquibase 같은 마이그레이션 도구 사용을 고려해주세요.
src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java (1)
48-56: (운영 관점) 유사상품 쿼리 성능을 위해 복합 인덱스 후보가 명확해 보여요Line 50-56 패턴은 보통
(on_sale, category_id, price)같은 복합 인덱스가 효율에 크게 기여합니다(데이터가 커질수록).src/main/java/com/ongil/backend/product/service/ProductService.java (1)
92-110: 유사상품은 현재 정렬이 없어 응답이 비결정적입니다(테스트/UX 관점에서 정렬 추가 추천)Line 100-101이
PageRequest.of(0, 6)만 사용해서 DB 기본 반환 순서에 의존합니다. 최소한popularity desc, id asc같은 정렬을 넣으면 결과가 안정적입니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/main/java/com/ongil/backend/domain/product/entity/Product.javasrc/main/java/com/ongil/backend/domain/product/enums/ProductSortType.javasrc/main/java/com/ongil/backend/domain/product/repository/ProductRepository.javasrc/main/java/com/ongil/backend/global/config/SecurityConfig.javasrc/main/java/com/ongil/backend/product/controller/ProductController.javasrc/main/java/com/ongil/backend/product/dto/converter/ProductConverter.javasrc/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.javasrc/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.javasrc/main/java/com/ongil/backend/product/service/ProductService.javasrc/main/resources/application-local.yml
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
SOLID 원칙, 스프링 어노테이션, 의존성 주입 패턴, 예외 처리에 중점을 둔다
Files:
src/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.javasrc/main/java/com/ongil/backend/global/config/SecurityConfig.javasrc/main/java/com/ongil/backend/domain/product/repository/ProductRepository.javasrc/main/java/com/ongil/backend/domain/product/entity/Product.javasrc/main/java/com/ongil/backend/product/dto/converter/ProductConverter.javasrc/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.javasrc/main/java/com/ongil/backend/product/controller/ProductController.javasrc/main/java/com/ongil/backend/domain/product/enums/ProductSortType.javasrc/main/java/com/ongil/backend/product/service/ProductService.java
**/*.{yml,yaml,properties}
⚙️ CodeRabbit configuration file
스프링 설정, 보안 설정, DB 연결, 환경 설정을 검증한다
Files:
src/main/resources/application-local.yml
🧬 Code graph analysis (1)
src/main/java/com/ongil/backend/product/service/ProductService.java (1)
src/main/java/com/ongil/backend/global/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(4-8)
🔇 Additional comments (6)
src/main/java/com/ongil/backend/domain/product/enums/ProductSortType.java (1)
6-15: LGTM!정렬 타입 enum이 깔끔하게 정의되었습니다. Lombok 어노테이션을 적절히 활용하고 있으며, description 필드를 통해 사용자 친화적인 표현을 제공합니다.
src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java (1)
6-14: LGTM!DTO 클래스 구조가 깔끔하고 Builder 패턴을 적절히 활용하고 있습니다.
src/main/java/com/ongil/backend/product/dto/converter/ProductConverter.java (1)
73-73: LGTM!
reviewCount필드가 올바르게 추가되었습니다. 기존 코드 스타일과 일관성을 유지하고 있습니다.src/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.java (1)
31-35:reviewCount추가는 OK, nullability(응답에서 null vs 0)만 팀 컨벤션으로 고정 추천Line 34의
Integer reviewCount는 확장 자체는 무리 없습니다. 다만 엔티티 기본값이 0인 만큼, 응답에서도 “항상 0 이상”을 보장할지(primitive/int 또는 변환 시 0 디폴트) “null 허용”을 할지 컨벤션을 정해두면 프론트 연동 시 혼선이 줄어요.
[Suggest_recommended_refactor]src/main/java/com/ongil/backend/product/controller/ProductController.java (2)
36-56: 목록 API 시그니처/위임 구조는 깔끔합니다.priceRange포맷 오류 시 에러 응답만 의도대로 떨어지는지 확인 필요Line 39-45에서
priceRange/clothingSize가 문자열이라, 파싱 실패 케이스가 있으면(예: “1~2만원” 같은 입력) 컨트롤러 레벨에서 어떤 에러로 내려가는지(400 vs 500)만 한번 확인해두면 안전합니다. (코딩 가이드라인 기준: 예외 처리 관점)
58-70: 특가/유사상품 엔드포인트 추가는 의도 명확하고 충돌도 없어 보여요Line 58-70의
/special-sale,/{productId}/similar는 경로도 직관적이고 기존/{productId}와도 충돌 가능성이 낮습니다.
| @Column(name = "review_count", nullable = false) | ||
| private Integer reviewCount = 0; | ||
|
|
||
| @Formula("view_count + purchase_count") | ||
| private Integer popularity; |
There was a problem hiding this comment.
DB 마이그레이션/백필 없으면 review_count NOT NULL에서 바로 터질 수 있어요 + @Formula는 COALESCE 권장
- Line 73-74:
review_count를nullable = false로 추가했으니, 기존 레코드 백필(0) + 컬럼 디폴트(0) 포함한 마이그레이션이 반드시 필요합니다(없으면 schema validate/insert 시점에 실패 가능). - Line 76-77:
view_count/purchase_count가 혹시라도 NULL인 데이터가 있으면 popularity가 NULL로 전파될 수 있어 정렬이 깨질 수 있습니다. COALESCE로 방어 추천.
제안 diff (COALESCE로 null-safe)
- @Formula("view_count + purchase_count")
+ @Formula("coalesce(view_count, 0) + coalesce(purchase_count, 0)")
private Integer popularity;🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/domain/product/entity/Product.java around
lines 73 - 77, You added reviewCount as a non-nullable column and defined
popularity with @Formula, which can break runtime/schema validation if existing
rows have NULL review_count or if view_count/purchase_count are NULL; create a
DB migration that backfills existing rows setting review_count = 0 and adds the
new column with DEFAULT 0 and NOT NULL, and update the @Formula on the
popularity field to be null-safe by using COALESCE for view_count and
purchase_count (e.g., COALESCE(view_count, 0) + COALESCE(purchase_count, 0)) so
popularity never becomes NULL.
| // 조건에 따른 상품 조회 | ||
| @EntityGraph(attributePaths = {"brand", "category"}) | ||
| @Query(""" | ||
| SELECT p FROM Product p | ||
| WHERE (:categoryId IS NULL OR p.category.id = :categoryId) | ||
| AND (:brandId IS NULL OR p.brand.id = :brandId) | ||
| AND (:minPrice IS NULL OR p.price >= :minPrice) | ||
| AND (:maxPrice IS NULL OR p.price <= :maxPrice) | ||
| AND (:size IS NULL OR p.sizes LIKE CONCAT('%', :size, '%')) | ||
| AND p.onSale = true | ||
| """) | ||
| Page<Product> findAllByCondition( | ||
| @Param("categoryId") Long categoryId, | ||
| @Param("brandId") Long brandId, | ||
| @Param("minPrice") Integer minPrice, | ||
| @Param("maxPrice") Integer maxPrice, | ||
| @Param("size") String size, | ||
| Pageable pageable | ||
| ); |
There was a problem hiding this comment.
sizes LIKE는 사용자 입력에 %/_가 들어오면 매칭이 과해질 수 있어요(escape 고려)
Line 29의 p.sizes LIKE CONCAT('%', :size, '%')는 기능적으로는 OK인데, 요청 파라미터에 %/_가 들어오면 와일드카드로 동작해서 “필터 우회”처럼 보일 수 있습니다(보안 이슈라기보단 정확도 이슈). 가능하면 escape 처리(입력 전처리 or JPQL ESCAPE) 쪽을 고려해볼만 합니다.
| .requestMatchers("/api/products/**").permitAll() | ||
| .requestMatchers("/api/brands/**").permitAll() | ||
| .requestMatchers("/api/categories/**").permitAll() |
There was a problem hiding this comment.
HTTP 메서드별로 접근 권한을 세분화하세요.
현재 /api/products/**, /api/brands/**, /api/categories/** 경로에 대해 모든 HTTP 메서드를 인증 없이 허용하고 있습니다. 이는 잠재적인 보안 취약점입니다:
- GET 요청: 조회는 public 접근 허용이 합리적
- POST/PUT/DELETE 요청: 생성/수정/삭제는 인증이 필요해야 함
현재 구현으로는 인증 없이 상품, 브랜드, 카테고리를 생성/수정/삭제할 수 있는 엔드포인트가 노출될 경우 보안 문제가 발생합니다.
🔒 HTTP 메서드별 권한 제어 제안
.authorizeHttpRequests(auth -> auth
.requestMatchers("/ping", "/h2-console/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/auth/logout", "/auth/withdraw").authenticated()
.requestMatchers("/auth/oauth/kakao", "/auth/oauth/google", "/auth/token/refresh").permitAll()
.requestMatchers("/auth/**").permitAll()
- .requestMatchers("/api/products/**").permitAll()
- .requestMatchers("/api/brands/**").permitAll()
- .requestMatchers("/api/categories/**").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/brands/**").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/categories/**").permitAll()
+ .requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
+ .requestMatchers(HttpMethod.PUT, "/api/products/**").hasRole("ADMIN")
+ .requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
+ // brands, categories도 동일하게 적용
.anyRequest().authenticated()
)HttpMethod import도 추가해야 합니다:
import org.springframework.http.HttpMethod;| public Integer[] parsePriceRange() { | ||
| if (priceRange == null || priceRange.trim().isEmpty()) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| String[] parts = priceRange.split("-"); | ||
| if (parts.length != 2) { | ||
| return null; | ||
| } | ||
|
|
||
| Integer minPrice = Integer.parseInt(parts[0].trim()); | ||
| Integer maxPrice = Integer.parseInt(parts[1].trim()); | ||
|
|
||
| return new Integer[] {minPrice, maxPrice}; | ||
| } catch (NumberFormatException e) { | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
가격 범위 검증 로직을 추가하세요.
parsePriceRange() 메서드에 다음과 같은 검증 누락이 있습니다:
- 최소-최대 가격 순서 검증 없음: "1000-500" 같은 잘못된 입력도 그대로 반환됩니다.
- 음수 가격 검증 없음: "-100-200" 같은 입력이 허용됩니다.
- 에러 처리 모호함: null 반환이 "필터 없음"인지 "잘못된 입력"인지 구분할 수 없습니다.
이로 인해 잘못된 필터링 결과가 발생하거나 디버깅이 어려워질 수 있습니다.
✅ 검증 로직 추가 제안
public Integer[] parsePriceRange() {
if (priceRange == null || priceRange.trim().isEmpty()) {
return null;
}
try {
String[] parts = priceRange.split("-");
if (parts.length != 2) {
return null;
}
Integer minPrice = Integer.parseInt(parts[0].trim());
Integer maxPrice = Integer.parseInt(parts[1].trim());
+
+ // 음수 가격 검증
+ if (minPrice < 0 || maxPrice < 0) {
+ return null;
+ }
+
+ // 가격 범위 순서 검증
+ if (minPrice > maxPrice) {
+ return null;
+ }
return new Integer[] {minPrice, maxPrice};
} catch (NumberFormatException e) {
return null;
}
}더 나아가, Optional이나 커스텀 예외를 사용하여 "필터 없음"과 "파싱 에러"를 명확히 구분하는 것도 고려해보세요.
🤖 Prompt for AI Agents
In
@src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
around lines 16 - 34, Update parsePriceRange() to validate order and
non-negativity and to distinguish no-filter from parse errors: when priceRange
is null/empty return Optional.empty() (or a sentinel indicating "no filter"),
otherwise parse trimmed parts into integers, reject if parts length != 2 or
parsing fails by throwing a clear IllegalArgumentException or returning a
distinct Optional.empty() vs Optional.ofError pattern; additionally validate
minPrice >= 0, maxPrice >= 0 and minPrice <= maxPrice before returning new
Integer[] {minPrice, maxPrice} (use the method name parsePriceRange() and its
return type to implement Optional or throw a specific exception so callers can
differentiate "no filter" vs "invalid input").
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @src/main/java/com/ongil/backend/product/service/ProductService.java:
- Around line 92-110: The Product loaded in getSimilarProducts uses
productRepository.findById(productId) which returns a LAZY category and causes
an N+1 when calling product.getCategory().getId(); change the call to use a
repository method that fetches category eagerly (e.g., add
findByIdWithCategory(Long id) or findByIdWithCategoryAndBrand if preferred on
ProductRepository annotated with @EntityGraph(attributePaths = {"category"}) or
a custom JPQL join fetch) and replace productRepository.findById(productId) with
that eager-loading method so product.getCategory().getId() does not trigger an
extra query.
🧹 Nitpick comments (1)
src/main/java/com/ongil/backend/domain/product/entity/Product.java (1)
73-74: Builder 패턴 일관성 검토 권장
reviewCount필드가 추가되었지만 Builder 생성자(94-120라인)에는 포함되지 않았습니다.viewCount,purchaseCount도 기본값을 가지고 있어 Builder에 포함되지 않은 패턴을 따르고 있지만, 데이터 마이그레이션이나 테스트 시나리오에서 초기 리뷰 수를 설정해야 할 경우 유연성이 떨어질 수 있습니다.필요 시 Builder에
reviewCount파라미터를 추가하는 것을 고려해보세요.♻️ Builder에 reviewCount 추가 예시
@Builder public Product(String name, String description, Integer price, String materialOriginal, String aiMaterialAdvantages, String aiMaterialDisadvantages, String aiMaterialCare, String washingMethod, String imageUrls, String sizes, String colors, Integer discountRate, - Integer discountPrice, ProductType productType, + Integer discountPrice, Integer reviewCount, ProductType productType, Brand brand, Category category) { this.name = name; this.description = description; this.price = price; this.materialOriginal = materialOriginal; this.aiMaterialAdvantages = aiMaterialAdvantages; this.aiMaterialDisadvantages = aiMaterialDisadvantages; this.aiMaterialCare = aiMaterialCare; this.washingMethod = washingMethod; this.imageUrls = imageUrls; this.sizes = sizes; this.colors = colors; this.discountRate = discountRate; this.discountPrice = discountPrice; + this.reviewCount = reviewCount != null ? reviewCount : 0; this.productType = productType; this.brand = brand; this.category = category; }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/com/ongil/backend/domain/product/entity/Product.javasrc/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.javasrc/main/java/com/ongil/backend/product/service/ProductService.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
🧰 Additional context used
📓 Path-based instructions (1)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
SOLID 원칙, 스프링 어노테이션, 의존성 주입 패턴, 예외 처리에 중점을 둔다
Files:
src/main/java/com/ongil/backend/product/service/ProductService.javasrc/main/java/com/ongil/backend/domain/product/entity/Product.java
🔇 Additional comments (5)
src/main/java/com/ongil/backend/product/service/ProductService.java (4)
28-28: 클래스 레벨 읽기 전용 트랜잭션 적용 승인클래스 레벨에
@Transactional(readOnly = true)를 적용한 것은 서비스 레이어의 모범 사례입니다.getProductDetail메서드가 개별적으로@Transactional로 오버라이드하여 조회수 증가 등의 쓰기 작업을 처리하는 구조도 올바릅니다.
83-90: 특가 상품 조회 로직 승인상위 10개 특가 상품을
discountRate내림차순으로 조회하는 로직이 명확하고 올바릅니다.PageRequest.of(0, 10, ...)을 사용하여 고정된 개수를 반환하는 방식도 PR 목표에 부합합니다.
54-81: N+1 쿼리 방지 검증: 이미 적절하게 구현됨
findAllByCondition메서드에@EntityGraph(attributePaths = {"brand", "category"})가 이미 적용되어 있으므로 Brand와 Category를 즉시 로딩하여 N+1 문제를 방지하고 있습니다. Product Entity의 LAZY 로딩 설정을 @EntityGraph가 올바르게 오버라이드하므로 추가 조치는 불필요합니다.
138-160: 다단계 tie-breaker 정렬 로직은 우수하며 @formula 필드 정렬도 정상 작동다단계 tie-breaker를 사용한 정렬 설계가 명확하고 우수합니다:
- POPULAR: popularity → id
- REVIEW: reviewCount → popularity → id
- PRICE_HIGH/LOW: price → popularity → id
popularity필드의@Formula사용과 관련하여, Spring Data JPA와 Hibernate는 이 패턴을 완전히 지원합니다. Formula 표현식이 SQL에 인라인되는 방식도 정상적이므로 정렬 기능은 예상대로 작동합니다.다만 Formula 표현식이 SELECT와 ORDER BY에 반복 포함될 수 있어 성능 고려사항이 있습니다. 현재 사용 중인 공식(
coalesce(view_count, 0) + coalesce(purchase_count, 0))은 간단한 산술 연산이라 성능 영향은 제한적입니다. 향후 대규모 트래픽 환경에서 필요하면 계산된 컬럼(generated column) 또는 물리화된 뷰(materialized view) 도입을 검토할 수 있습니다.src/main/java/com/ongil/backend/domain/product/entity/Product.java (1)
76-77: @formula 기반 popularity 필드는 올바르게 구현됨
@Formula로 정의된 popularity 필드가 ProductService의 4개 정렬 로직(POPULAR, REVIEW, PRICE_HIGH, PRICE_LOW)에서 secondary 정렬 기준으로 사용 중입니다. Hibernate는 @formula 필드를 ORDER BY 절에서 올바르게 처리하며, 데이터베이스 수준 연산을 통해 N+1 문제를 방지하는 이 패턴은 검증된 구현입니다.
| public List<ProductSimpleResponse> getSimilarProducts(Long productId) { | ||
| Product product = productRepository.findById(productId) | ||
| .orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_NOT_FOUND)); | ||
|
|
||
| int basePrice = product.getPrice(); | ||
| int minPrice = (int)(basePrice * 0.8); | ||
| int maxPrice = (int)(basePrice * 1.2); | ||
|
|
||
| Pageable pageable = PageRequest.of(0, 6); | ||
| Page<Product> similarProducts = productRepository.findByOnSaleTrueAndCategoryIdAndIdNotAndPriceBetween( | ||
| product.getCategory().getId(), | ||
| productId, | ||
| minPrice, | ||
| maxPrice, | ||
| pageable | ||
| ); | ||
|
|
||
| return similarProducts.map(productConverter::toSimpleResponse).getContent(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find src -type f -name "*ProductRepository*" | head -20Repository: IT-Cotato/12th-OnGil-BE
Length of output: 147
🏁 Script executed:
cat -n src/main/java/com/ongil/backend/product/service/ProductService.java | sed -n '92,110p'Repository: IT-Cotato/12th-OnGil-BE
Length of output: 857
🏁 Script executed:
# Find ProductRepository file
fd "ProductRepository" src --type fRepository: IT-Cotato/12th-OnGil-BE
Length of output: 147
🏁 Script executed:
# Check the directory structure for repository files
ls -la src/main/java/com/ongil/backend/product/repository/ 2>/dev/null || ls -la src/main/java/com/ongil/backend/*/repository/ 2>/dev/null | head -30Repository: IT-Cotato/12th-OnGil-BE
Length of output: 49
🏁 Script executed:
cat -n src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.javaRepository: IT-Cotato/12th-OnGil-BE
Length of output: 2385
🏁 Script executed:
# Find Product entity file
fd "Product.java" src --type f | grep -v RepositoryRepository: IT-Cotato/12th-OnGil-BE
Length of output: 133
🏁 Script executed:
cat -n src/main/java/com/ongil/backend/domain/product/entity/Product.java | head -100Repository: IT-Cotato/12th-OnGil-BE
Length of output: 3478
🏁 Script executed:
# Check if findById is overridden in ProductRepository
rg -n "findById|@EntityGraph" src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.javaRepository: IT-Cotato/12th-OnGil-BE
Length of output: 237
🏁 Script executed:
# Check for any eager fetch in Product entity
rg -n "fetch.*EAGER|fetch.*LAZY|@ManyToOne|@OneToMany" src/main/java/com/ongil/backend/domain/product/entity/Product.javaRepository: IT-Cotato/12th-OnGil-BE
Length of output: 144
findById 호출 시 Category 즉시 로딩 필요
102라인의 product.getCategory().getId()에서 N+1 쿼리가 발생합니다. 93라인의 productRepository.findById(productId) 호출 시 Category가 지연 로딩(LAZY) 상태로 반환되므로, 카테고리 ID 접근 시 추가 쿼리가 트리거됩니다.
해결 방법:
- ProductRepository에 EntityGraph를 적용한 findById 커스텀 메서드 추가
- 또는
findById대신 category를 포함하는 커스텀 조회 메서드 사용
참고: findByOnSaleTrueAndCategoryIdAndIdNotAndPriceBetween 메서드는 이미 @EntityGraph(attributePaths = {"brand", "category"})로 보호되어 있으므로 해당 쿼리는 N+1 문제가 없습니다.
🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/product/service/ProductService.java around
lines 92 - 110, The Product loaded in getSimilarProducts uses
productRepository.findById(productId) which returns a LAZY category and causes
an N+1 when calling product.getCategory().getId(); change the call to use a
repository method that fetches category eagerly (e.g., add
findByIdWithCategory(Long id) or findByIdWithCategoryAndBrand if preferred on
ProductRepository annotated with @EntityGraph(attributePaths = {"category"}) or
a custom JPQL join fetch) and replace productRepository.findById(productId) with
that eager-loading method so product.getCategory().getId() does not trigger an
extra query.
🔍️ 작업 내용
상품 목록 (페이징, 필터, 정렬), 특가 상품 목록(할인율 높은 순으로 10개 정렬), 비슷한 상품 6개(같은 카테고리 + 비슷한 가격대)
✨ 상세 설명
🛠️ 추후 리팩토링 및 고도화 계획
프론트와 api 연동 후 이상한 부분 있을시 수정
redis 사용 여부 생각 후 필요시 추가
📸 스크린샷 (선택)
💬 리뷰 요구사항
Summary by CodeRabbit
새로운 기능
Chores
✏️ Tip: You can customize this high-level summary in your review settings.