Skip to content

Commit 4d3cd22

Browse files
authored
[Fix] 상품 조회 시 N+1 쿼리 문제 해결 (#328)
* work * work * work
1 parent ec857ff commit 4d3cd22

File tree

3 files changed

+49
-26
lines changed

3 files changed

+49
-26
lines changed

src/main/java/com/back/domain/product/product/repository/ProductCustomRepositoryImpl.java

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,24 +80,13 @@ public ProductListResponse findProducts(
8080
));
8181
}
8282

83-
// QueryDSL로 DTO 매핑 + THUMBNAIL join
83+
// QueryDSL로 엔티티 조회 + THUMBNAIL join
8484
var query = queryFactory
85-
.select(Projections.constructor(
86-
ProductInfo.class, // dto 매핑
87-
p.productUuid, // 상품 uuid
88-
img.fileUrl, //썸네일 이미지 url
89-
p.brandName, // 브랜드명
90-
p.name, // 상품명
91-
p.price, // 가격
92-
p.discountRate,// 할인율
93-
p.price.subtract(p.price.multiply(p.discountRate).divide(100)), // 할인된 최종 가격
94-
Expressions.nullExpression(Double.class) // rating: 리뷰 연동 전이라서 일단 null로 함.
95-
))
85+
.select(p)
9686
.from(p)
97-
.leftJoin(p.images, img) // left join
98-
.on(img.fileType.eq(FileType.THUMBNAIL)) // type이 THUMBNAIL인 이미지만 JOIN
99-
.where(builder) // 동적 조건 적용
100-
.distinct(); // 중복 제거
87+
.leftJoin(p.images, img).on(img.fileType.eq(FileType.THUMBNAIL))
88+
.where(builder)
89+
.distinct();
10190

10291
// 정렬 처리
10392
if ("priceAsc".equals(sort)) query.orderBy(p.price.asc()); // 가격 낮은 순
@@ -107,12 +96,35 @@ public ProductListResponse findProducts(
10796
// 전체 건수 조회 (페이징용)
10897
long total = query.fetchCount();
10998

110-
// 페이징 적용
111-
List<ProductInfo> products = query
112-
.offset(pageable.getOffset()) // offset
113-
.limit(pageable.getPageSize()) //limit
99+
// 페이징 적용하여 엔티티 조회
100+
List<com.back.domain.product.product.entity.Product> fetchedProducts = query
101+
.offset(pageable.getOffset())
102+
.limit(pageable.getPageSize())
114103
.fetch();
115104

105+
// 엔티티를 DTO로 변환
106+
List<ProductInfo> products = fetchedProducts.stream()
107+
.map(product -> {
108+
String thumbnailUrl = product.getImages().stream()
109+
.filter(i -> i.getFileType() == FileType.THUMBNAIL)
110+
.findFirst()
111+
.map(com.back.domain.product.product.entity.ProductImage::getFileUrl)
112+
.orElse(null);
113+
114+
return new ProductInfo(
115+
product.getProductUuid(),
116+
thumbnailUrl,
117+
product.getBrandName(),
118+
product.getName(),
119+
product.getPrice(),
120+
product.getDiscountRate(),
121+
product.getDiscountPrice(),
122+
product.getAverageRating()
123+
);
124+
})
125+
.toList();
126+
127+
116128
int totalPages = (int) Math.ceil((double) total / pageable.getPageSize());
117129

118130
// 최종 DTO 반환

src/main/java/com/back/domain/product/product/repository/ProductRepository.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,43 +35,49 @@ public interface ProductRepository extends JpaRepository<Product, Long>, Product
3535
List<Product> findBySellingEndDateBefore(LocalDateTime dateTime);
3636

3737
// 신상품 (최근 14일)
38-
@Query("SELECT p FROM Product p " +
38+
@Query("SELECT DISTINCT p FROM Product p " +
39+
"LEFT JOIN FETCH p.images " +
3940
"WHERE p.createDate BETWEEN :fromDate AND :toDate " +
4041
"AND p.displayStatus = com.back.domain.product.product.entity.DisplayStatus.DISPLAYING " +
4142
"ORDER BY p.createDate DESC")
4243
List<Product> findRecentProducts(@Param("fromDate") LocalDateTime fromDate,
4344
@Param("toDate") LocalDateTime toDate);
4445

4546
// 할인중 상품
46-
@Query("SELECT p FROM Product p " +
47+
@Query("SELECT DISTINCT p FROM Product p " +
48+
"LEFT JOIN FETCH p.images " +
4749
"WHERE p.discountRate > 0 " +
4850
"AND p.displayStatus = com.back.domain.product.product.entity.DisplayStatus.DISPLAYING " +
4951
"ORDER BY p.discountRate DESC")
5052
List<Product> findOnSaleProducts();
5153

5254
// 품절 임박 상품 (재고 5개 이하)
53-
@Query("SELECT p FROM Product p " +
55+
@Query("SELECT DISTINCT p FROM Product p " +
56+
"LEFT JOIN FETCH p.images " +
5457
"WHERE p.stock <= 5 " +
5558
"AND p.displayStatus = com.back.domain.product.product.entity.DisplayStatus.DISPLAYING " +
5659
"ORDER BY p.stock ASC")
5760
List<Product> findLowStockProducts();
5861

5962
// 재입고 상품
60-
@Query("SELECT p FROM Product p " +
63+
@Query("SELECT DISTINCT p FROM Product p " +
64+
"LEFT JOIN FETCH p.images " +
6165
"WHERE p.isRestock = true " +
6266
"AND p.displayStatus = com.back.domain.product.product.entity.DisplayStatus.DISPLAYING " +
6367
"ORDER BY p.createDate DESC")
6468
List<Product> findRestockProducts();
6569

6670
// 기획 상품
67-
@Query("SELECT p FROM Product p " +
71+
@Query("SELECT DISTINCT p FROM Product p " +
72+
"LEFT JOIN FETCH p.images " +
6873
"WHERE p.isPlanned = true " +
6974
"AND p.displayStatus = com.back.domain.product.product.entity.DisplayStatus.DISPLAYING " +
7075
"ORDER BY p.createDate DESC")
7176
List<Product> findPlannedProducts();
7277

7378
// 오픈 예정 상품
74-
@Query("SELECT p FROM Product p " +
79+
@Query("SELECT DISTINCT p FROM Product p " +
80+
"LEFT JOIN FETCH p.images " +
7581
"WHERE p.sellingStartDate > :today " +
7682
"AND p.displayStatus = com.back.domain.product.product.entity.DisplayStatus.DISPLAYING " +
7783
"ORDER BY p.sellingStartDate ASC")

src/main/java/com/back/domain/product/product/service/ProductService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,34 +152,39 @@ public List<ProductListResponse.ProductInfo> getAllNewProducts() {
152152
}
153153

154154
/** 메인페이지 - 할인중 상품 */
155+
@Transactional(readOnly = true)
155156
public List<ProductListResponse.ProductInfo> getOnSaleProducts() {
156157
List<Product> products = productRepository.findOnSaleProducts();
157158
if (products.isEmpty()) throw new ServiceException("404", "할인중인 상품이 존재하지 않습니다.");
158159
return products.stream().map(this::toProductInfo).toList();
159160
}
160161

161162
/** 메인페이지 - 품절 임박 상품 */
163+
@Transactional(readOnly = true)
162164
public List<ProductListResponse.ProductInfo> getLowStockProducts() {
163165
List<Product> products = productRepository.findLowStockProducts();
164166
if (products.isEmpty()) throw new ServiceException("404", "품절 임박 상품이 존재하지 않습니다.");
165167
return products.stream().map(this::toProductInfo).toList();
166168
}
167169

168170
/** 메인페이지 - 재입고 상품 */
171+
@Transactional(readOnly = true)
169172
public List<ProductListResponse.ProductInfo> getRestockProducts() {
170173
List<Product> products = productRepository.findRestockProducts();
171174
if (products.isEmpty()) throw new ServiceException("404", "재입고 상품이 존재하지 않습니다.");
172175
return products.stream().map(this::toProductInfo).toList();
173176
}
174177

175178
/** 메인페이지 - 기획 상품 */
179+
@Transactional(readOnly = true)
176180
public List<ProductListResponse.ProductInfo> getPlannedProducts() {
177181
List<Product> products = productRepository.findPlannedProducts();
178182
if (products.isEmpty()) throw new ServiceException("404", "기획 상품이 존재하지 않습니다.");
179183
return products.stream().map(this::toProductInfo).toList();
180184
}
181185

182186
/** 메인페이지 - 오픈 예정 상품 */
187+
@Transactional(readOnly = true)
183188
public List<ProductListResponse.ProductInfo> getUpcomingProducts() {
184189
LocalDateTime today = LocalDateTime.now();
185190
List<Product> products = productRepository.findUpcomingProducts(today);

0 commit comments

Comments
 (0)