Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] refactor: 상품 목록 조회 api 성능 개선 #685

Merged
merged 20 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dde67cc
refactor: ProductsInCategoryResponse에서 PageDto(페이징에 대한 자세한 정보)를 제거하고 …
hanueleee Sep 19, 2023
1055b4c
refactor: ProductRepository의 findAllByCategory와 findAllByCategoryByRe…
hanueleee Sep 19, 2023
18538fb
refactor: ProductService의 reviewCount로 인한 분기 처리 부분 제거 및 PageDto대신 has…
hanueleee Sep 19, 2023
f97f332
test: 상품 목록 조회 인수테스트에서 페이지 검증 대신 다음 페이지 유무를 검증하도록 수정
hanueleee Sep 19, 2023
457db97
test: findAllByCategoryOrderByReviewCountDesc테스트를 findAllByCategory 테…
hanueleee Sep 19, 2023
445a26c
feat: 상품목록조회 api 수정사항 반영
hanueleee Sep 20, 2023
73fd56c
feat: 정렬 조건별 메소드 생성
hanueleee Sep 20, 2023
37a7bbe
feat: ProductService에서 정렬 조건별 분기처리
hanueleee Sep 20, 2023
13640f1
test: 상품목록조회api 변경사항 인수테스트에 반영
hanueleee Sep 20, 2023
e589031
chore: toString 제거
hanueleee Sep 20, 2023
8c423d1
fix: findProductByReviewCountDesc 메소드 오류 수정
hanueleee Sep 21, 2023
501b254
feat: specification을 이용한 동적 쿼리 적용
hanueleee Oct 15, 2023
04df132
feat: count 쿼리 안나가도록 수정
hanueleee Oct 15, 2023
e9e658a
refactor: 정렬조건(sortBy, sortOrder)용 dto인 ProductSortCondition 추가
hanueleee Oct 15, 2023
25e449c
refactor: ProductSpecification의 메소드명 변경
hanueleee Oct 15, 2023
8c186e9
Merge branch 'develop' into feat/issue-669
hanueleee Oct 16, 2023
f0553fa
refactor: 리뷰 반영
hanueleee Oct 17, 2023
312d431
chore: 충돌 해결
hanueleee Oct 17, 2023
04e72f9
Merge branch 'develop' into feat/issue-669
hanueleee Oct 17, 2023
9d9008a
fix: 테스트 fail 해결
hanueleee Oct 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.funeat.common.repository;

import java.io.Serializable;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
Expand All @@ -11,4 +12,6 @@
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {

Page<T> findAllForPagination(final Specification<T> spec, final Pageable pageable, final Long totalElements);

List<T> findAllWithSpecification(final Specification<T> spec, final int pageSize);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.funeat.common.repository;

import java.io.Serializable;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
Expand Down Expand Up @@ -37,4 +40,12 @@ public Page<T> findAllForPagination(final Specification<T> spec, final Pageable

return new PageImpl<>(query.getResultList(), PageRequest.of(0, pageSize), totalElements);
}

@Override
public List<T> findAllWithSpecification(final Specification<T> spec, final int pageSize) {
final TypedQuery<T> query = getQuery(spec, Sort.unsorted());
query.setMaxResults(pageSize);

return query.getResultList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductResponse;
import com.funeat.product.dto.ProductReviewCountDto;
import com.funeat.product.dto.ProductSortCondition;
import com.funeat.product.dto.ProductsInCategoryResponse;
import com.funeat.product.dto.RankingProductDto;
import com.funeat.product.dto.RankingProductsResponse;
Expand All @@ -21,6 +22,7 @@
import com.funeat.product.persistence.CategoryRepository;
import com.funeat.product.persistence.ProductRecipeRepository;
import com.funeat.product.persistence.ProductRepository;
import com.funeat.product.persistence.ProductSpecification;
import com.funeat.recipe.domain.Recipe;
import com.funeat.recipe.domain.RecipeImage;
import com.funeat.recipe.dto.RecipeDto;
Expand All @@ -32,11 +34,11 @@
import com.funeat.tag.domain.Tag;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -46,8 +48,9 @@ public class ProductService {

private static final int THREE = 3;
private static final int TOP = 0;
public static final String REVIEW_COUNT = "reviewCount";
private static final int RANKING_SIZE = 3;
private static final int DEFAULT_PAGE_SIZE = 10;
private static final int DEFAULT_CURSOR_PAGINATION_SIZE = 11;

private final CategoryRepository categoryRepository;
private final ProductRepository productRepository;
Expand All @@ -60,7 +63,8 @@ public class ProductService {
public ProductService(final CategoryRepository categoryRepository, final ProductRepository productRepository,
final ReviewTagRepository reviewTagRepository, final ReviewRepository reviewRepository,
final ProductRecipeRepository productRecipeRepository,
final RecipeImageRepository recipeImageRepository, final RecipeRepository recipeRepository) {
final RecipeImageRepository recipeImageRepository,
final RecipeRepository recipeRepository) {
this.categoryRepository = categoryRepository;
this.productRepository = productRepository;
this.reviewTagRepository = reviewTagRepository;
Expand All @@ -70,25 +74,39 @@ public ProductService(final CategoryRepository categoryRepository, final Product
this.recipeRepository = recipeRepository;
}

public ProductsInCategoryResponse getAllProductsInCategory(final Long categoryId,
final Pageable pageable) {
public ProductsInCategoryResponse getAllProductsInCategory(final Long categoryId, final Long lastProductId,
final ProductSortCondition sortCondition) {
final Category category = categoryRepository.findById(categoryId)
.orElseThrow(() -> new CategoryNotFoundException(CATEGORY_NOT_FOUND, categoryId));
final Product lastProduct = productRepository.findById(lastProductId).orElse(null);

final Page<ProductInCategoryDto> pages = getAllProductsInCategory(pageable, category);
final Specification<Product> specification = ProductSpecification.searchBy(category, lastProduct, sortCondition);
final List<Product> findResults = productRepository.findAllWithSpecification(specification, DEFAULT_CURSOR_PAGINATION_SIZE);

final PageDto pageDto = PageDto.toDto(pages);
final List<ProductInCategoryDto> productDtos = pages.getContent();
final List<ProductInCategoryDto> productDtos = getProductInCategoryDtos(findResults);
final boolean hasNext = hasNextPage(findResults);

return ProductsInCategoryResponse.toResponse(pageDto, productDtos);
return ProductsInCategoryResponse.toResponse(hasNext, productDtos);
}

private Page<ProductInCategoryDto> getAllProductsInCategory(final Pageable pageable, final Category category) {
if (Objects.nonNull(pageable.getSort().getOrderFor(REVIEW_COUNT))) {
final PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
return productRepository.findAllByCategoryOrderByReviewCountDesc(category, pageRequest);
private List<ProductInCategoryDto> getProductInCategoryDtos(final List<Product> findProducts) {
final int resultSize = getResultSize(findProducts);
final List<Product> products = findProducts.subList(0, resultSize);

return products.stream()
.map(ProductInCategoryDto::toDto)
.collect(Collectors.toList());
}

private int getResultSize(final List<Product> findProducts) {
if (findProducts.size() < DEFAULT_CURSOR_PAGINATION_SIZE) {
return findProducts.size();
}
return productRepository.findAllByCategory(category, pageable);
return DEFAULT_PAGE_SIZE;
}

private boolean hasNextPage(final List<Product> findProducts) {
return findProducts.size() > DEFAULT_PAGE_SIZE;
}

public ProductResponse findProductDetail(final Long productId) {
Expand Down
10 changes: 10 additions & 0 deletions backend/src/main/java/com/funeat/product/domain/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ public Product(final String name, final Long price, final String image, final St
this.category = category;
}

public Product(final String name, final Long price, final String image, final String content,
final Category category, final Long reviewCount) {
this.name = name;
this.price = price;
this.image = image;
this.content = content;
this.category = category;
this.reviewCount = reviewCount;
}

public static Product create(final String name, final Long price, final String content, final Category category) {
return new Product(name, price, null, content, category);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ public ProductInCategoryDto(final Long id, final String name, final Long price,
this.reviewCount = reviewCount;
}

public static ProductInCategoryDto toDto(final Product product) {
return new ProductInCategoryDto(product.getId(), product.getName(), product.getPrice(), product.getImage(),
product.getAverageRating(), product.getReviewCount());
}

public static ProductInCategoryDto toDto(final Product product, final Long reviewCount) {
return new ProductInCategoryDto(product.getId(), product.getName(), product.getPrice(), product.getImage(),
product.getAverageRating(), reviewCount);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.funeat.product.dto;

public class ProductSortCondition {

private final String by;
private final String order;

private ProductSortCondition(final String by, final String order) {
this.by = by;
this.order = order;
}

public static ProductSortCondition toDto(final String sort) {
final String[] split = sort.split(",");
return new ProductSortCondition(split[0], split[1]);
}

public String getBy() {
return by;
}

public String getOrder() {
return order;
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
package com.funeat.product.dto;

import com.funeat.common.dto.PageDto;
import java.util.List;

public class ProductsInCategoryResponse {

private final PageDto page;
private final boolean hasNext;
private final List<ProductInCategoryDto> products;

public ProductsInCategoryResponse(final PageDto page, final List<ProductInCategoryDto> products) {
this.page = page;
public ProductsInCategoryResponse(final boolean hasNext, final List<ProductInCategoryDto> products) {
this.hasNext = hasNext;
this.products = products;
}

public static ProductsInCategoryResponse toResponse(final PageDto page, final List<ProductInCategoryDto> products) {
return new ProductsInCategoryResponse(page, products);
public static ProductsInCategoryResponse toResponse(final boolean hasNext,
final List<ProductInCategoryDto> products) {
return new ProductsInCategoryResponse(hasNext, products);
}

public PageDto getPage() {
return page;
public boolean isHasNext() {
return hasNext;
}

public List<ProductInCategoryDto> getProducts() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
public enum ProductErrorCode {

PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 상품입니다. 상품 id를 확인하세요.", "1001"),
NOT_SUPPORTED_PRODUCT_SORTING_CONDITION(HttpStatus.BAD_REQUEST, "정렬 조건이 올바르지 않습니다. 정렬 조건을 확인하세요", "1002");
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ public ProductNotFoundException(final ProductErrorCode errorCode, final Long pro
super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), productId));
}
}

public static class NotSupportedProductSortingConditionException extends ProductException {
public NotSupportedProductSortingConditionException(final ProductErrorCode errorCode, final String sortBy) {
super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), sortBy));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,15 @@
package com.funeat.product.persistence;

import com.funeat.product.domain.Category;
import com.funeat.common.repository.BaseRepository;
import com.funeat.product.domain.Product;
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductReviewCountDto;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {

@Query(value = "SELECT new com.funeat.product.dto.ProductInCategoryDto(p.id, p.name, p.price, p.image, p.averageRating, COUNT(r)) "
+ "FROM Product p "
+ "LEFT JOIN p.reviews r "
+ "WHERE p.category = :category "
+ "GROUP BY p ",
countQuery = "SELECT COUNT(p) FROM Product p WHERE p.category = :category")
Page<ProductInCategoryDto> findAllByCategory(@Param("category") final Category category, final Pageable pageable);

@Query(value = "SELECT new com.funeat.product.dto.ProductInCategoryDto(p.id, p.name, p.price, p.image, p.averageRating, COUNT(r)) "
+ "FROM Product p "
+ "LEFT JOIN p.reviews r "
+ "WHERE p.category = :category "
+ "GROUP BY p "
+ "ORDER BY COUNT(r) DESC, p.id DESC ",
countQuery = "SELECT COUNT(p) FROM Product p WHERE p.category = :category")
Page<ProductInCategoryDto> findAllByCategoryOrderByReviewCountDesc(@Param("category") final Category category,
final Pageable pageable);
public interface ProductRepository extends BaseRepository<Product, Long> {

@Query("SELECT new com.funeat.product.dto.ProductReviewCountDto(p, COUNT(r.id)) "
+ "FROM Product p "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.funeat.product.persistence;

import static com.funeat.product.exception.ProductErrorCode.NOT_SUPPORTED_PRODUCT_SORTING_CONDITION;

import com.funeat.product.domain.Category;
import com.funeat.product.domain.Product;
import com.funeat.product.dto.ProductSortCondition;
import com.funeat.product.exception.ProductException.NotSupportedProductSortingConditionException;
import java.util.Objects;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;

public class ProductSpecification {
hanueleee marked this conversation as resolved.
Show resolved Hide resolved

private ProductSpecification() {
}

private static final String DESC = "desc";
private static final String CATEGORY = "category";
private static final String ID = "id";
private static final String REVIEW_COUNT = "reviewCount";
private static final String AVERAGE_RATING = "averageRating";
private static final String PRICE = "price";

public static Specification<Product> searchBy(final Category category, final Product lastProduct,
final ProductSortCondition sortCondition) {
return (root, query, builder) -> {
setOrderBy(sortCondition, root, query, builder);

return Specification
.where(sameCategory(category))
.and(nextCursor(lastProduct, sortCondition))
.toPredicate(root, query, builder);
};
}

private static void setOrderBy(final ProductSortCondition sortCondition, final Root<Product> root,
final CriteriaQuery<?> query, final CriteriaBuilder builder) {
final String sortBy = sortCondition.getBy();
final String sortOrder = sortCondition.getOrder();

if (DESC.equals(sortOrder)) {
query.orderBy(builder.desc(root.get(sortBy)), builder.desc(root.get(ID)));
} else {
query.orderBy(builder.asc(root.get(sortBy)), builder.desc(root.get(ID)));
}
}

private static Specification<Product> sameCategory(final Category category) {
return (root, query, builder) -> {
final Path<Object> categoryPath = root.get(CATEGORY);

return builder.equal(categoryPath, category);
};
}

private static Specification<Product> nextCursor(final Product lastProduct, final ProductSortCondition sortCondition) {
final String sortBy = sortCondition.getBy();
final String sortOrder = sortCondition.getOrder();

return (root, query, builder) -> {
if (Objects.isNull(lastProduct)) {
return null;
}

final Comparable comparisonValue = (Comparable) getComparisonValue(lastProduct, sortBy);

return builder.or(
sameValueAndSmallerId(sortBy, lastProduct.getId(), comparisonValue).toPredicate(root, query, builder),
nextValue(sortBy, sortOrder, comparisonValue).toPredicate(root, query, builder)
);
};
}

private static Object getComparisonValue(final Product lastProduct, final String sortBy) {
if (PRICE.equals(sortBy)) {
return lastProduct.getPrice();
}
if (AVERAGE_RATING.equals(sortBy)) {
return lastProduct.getAverageRating();
}
if (REVIEW_COUNT.equals(sortBy)) {
return lastProduct.getReviewCount();
}
throw new NotSupportedProductSortingConditionException(NOT_SUPPORTED_PRODUCT_SORTING_CONDITION, sortBy);
}

private static Specification<Product> sameValueAndSmallerId(final String sortBy, final Long lastProductId,
final Comparable comparisonValue) {
return (root, query, builder) -> builder.and(
builder.equal(root.get(sortBy), comparisonValue),
builder.lessThan(root.get(ID), lastProductId));
}

private static Specification<Product> nextValue(final String sortBy, final String sortOrder,
final Comparable comparisonValue) {
return (root, query, builder) -> {
if (DESC.equals(sortOrder)) {
return builder.lessThan(root.get(sortBy), comparisonValue);
} else {
return builder.greaterThan(root.get(sortBy), comparisonValue);
}
};
}
}
Loading