프로젝트에서 리뷰와 코멘트를 담당으로 진행하였으며
ERD는 아래와 같이 설계를 진행했다.
리뷰와 코멘트는 1대1관계이고 Comment가 review_id를 갖는 관계이다.
여기서 가게기준으로 리뷰와 코멘트를 한번에 출력될 수 있도록 조회 기능을 만드는 것이 문제였는데,
처음 구현했던 코드의 문제점이 보여 리팩토링을 진행했다.
기존 코드의 문제점
1. N+1 문제 : reviewRepository를 통해 리뷰 리스트를 가져온 후, 각 리뷰에 대해 commentRepository를 사용해 코멘트를 개별적으로 조회, 리뷰 13건과 코멘트 13건 기준으로 총 27개의 쿼리(1개는 가게 확인 쿼리)가 실행되어 데이터베이스의 부하를 증가시키고 성능을 저하시킴
2. 각 리뷰마다 별도의 쿼리가 실행되면서 네트워크 I/O가 불필요하게 많이 발생
--ReviewRepository--
List<Review> findAllByShopShopIdAndStarPointBetweenOrderByCreatedAtDesc(Long shopId,
Long starPointStart, Long starPointEnd);
--ReviewService--
public List<ReviewListDto> getShopsssReview(Long shopId, Long minStarPoint, Long maxStarPoint) {
if (shopRepository.findByShopIdAndIsClosing(shopId).isEmpty()) {
throw new IllegalArgumentException("Shop not found with ID:" + shopId);
}
List<Review> reviewList = reviewRepository.findAllByShopShopIdAndStarPointBetweenOrderByCreatedAtDesc(
shopId, minStarPoint, maxStarPoint);
//TODO 해당 로직이 리뷰 한건당 쿼리 하나씩 추가로 날려서 고민해보고 로직 변경 예정
return reviewList.stream().map(
review -> ReviewListDto.builder().userId(review.getUser().getUserId())
.createdAt(review.getCreatedAt()).updatedAt(review.getLastModifiedAt())
.starPoint(review.getStarPoint()).reviewContent(review.getReviewContent())
.shopId(review.getShop().getShopId()).reviewId(review.getReviewId())
.purchaseId(review.getPurchase().getPurchaseId()).comment(
Optional.ofNullable(commentRepository.findByReviewReviewId(review.getReviewId()))
.map(CommentReviewDto::convertDto).orElse(null)).build()).toList();
}
수정한 코드
1. 쿼리 최적화 : JPQL을 사용해 리뷰와 코멘트를 동시에 가져오면서 필요한 정보만 가져오도록 변경
2. N+1 문제 해결 : 한번에 조회해서 문제 제거, 리뷰 13건 코멘트 13건 기준으로 쿼리가 2번만 실행됨
3. 실행 시간 단축 : 실험 결과, 리뷰 3건과 코멘트 3건 기준으로 실행 시간이 143ms에서 3ms로 단축되었으며, 리뷰 13건과 코멘트 13건 기준으로는 176ms에서 100ms로 개선
-- ReviewRepository --
@Query("""
SELECT new Not.Delivered.review.domain.Dto.ReviewWithCommentDto(
r.reviewId,
r.user.userId,
r.createdAt,
r.lastModifiedAt,
r.starPoint,
r.reviewContent,
r.shop.shopId,
r.purchase.purchaseId,
(SELECT c.commentContent FROM Comment c WHERE c.review.reviewId = r.reviewId)
)
FROM Review r
WHERE r.shop.shopId = :shopId
AND r.starPoint BETWEEN :minStarPoint AND :maxStarPoint
ORDER BY r.createdAt DESC
""")
List<ReviewWithCommentDto> findReviewsWithComment(Long shopId, Long minStarPoint,
Long maxStarPoint);
-- ReviewService --
public List<ReviewWithCommentDto> getShopReview(Long shopId, Long minStarPoint,
Long maxStarPoint) {
if (shopRepository.findByShopIdAndIsClosing(shopId).isEmpty()) {
throw new IllegalArgumentException("Shop not found with ID: " + shopId);
}
// DTO 리스트를 가져옴
List<ReviewWithCommentDto> results = reviewRepository.findReviewsWithComment(shopId,
minStarPoint, maxStarPoint);
// ReviewListDto로 변환하여 반환
return results.stream()
.map(dto -> ReviewWithCommentDto.builder()
.reviewId(dto.getReviewId())
.userId(dto.getUserId())
.createdAt(dto.getCreatedAt())
.lastModifiedAt(dto.getLastModifiedAt())
.starPoint(dto.getStarPoint())
.reviewContent(dto.getReviewContent())
.shopId(dto.getShopId())
.purchaseId(dto.getPurchaseId())
.commentContent(dto.getCommentContent())
.build())
.toList();
}
중간과정
1. Review와 Comment를 별도로 가져와 로직에서 매핑하는 방법 검토
처음에는 리뷰 데이터를 모두 조회한 뒤, 해당 가게의 모든 코멘트를 한 번에 가져와 애플리케이션 로직에서 리뷰와 코멘트를 매핑하는 방식을 고려. 하지만 이 방식은 데이터베이스 쿼리 호출 수를 줄일 순 있어도 애플리케이션 레벨에서 매핑 작업으로 인해 추가적인 처리 비용이 발생할 가능성이 있음 > 다른 방식 검토
2. Comment 데이터를 쿼리로 직접 가져오려 했으나 에러 발생 > 데이터베이스 설계상 불가능하여 일부 데이터만 반환하도록 쿼리 조정
'Project' 카테고리의 다른 글
티켓 발급 서비스에서 JPA 낙관적 락 구현 (0) | 2025.03.27 |
---|---|
[newsfeed] Like 기능의 성능 개선하기 (0) | 2024.12.30 |
[newsfeed] 댓글과 게시글의 '좋아요' 기능, 어떤 식으로 구현하는게 좋을까? (0) | 2024.12.30 |
[Todolist] ToDoList 과제 1차 TIL - 필수 구현까지 (0) | 2024.12.05 |