본문 바로가기
Project

[404 NOT DELIVERY] Review 가게별 조회 기능 개선하기

by 어떻게말이름이히힝 2025. 1. 13.

프로젝트에서 리뷰와 코멘트를 담당으로 진행하였으며 

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 데이터를 쿼리로 직접 가져오려 했으나 에러 발생 > 데이터베이스 설계상 불가능하여 일부 데이터만 반환하도록 쿼리 조정