본문 바로가기
개발/Spring

[N+1 문제] 게시글 댓글 조회 기능 querydsl로 해결하기

by meanjung 2023. 8. 6.

시나리오

Review(독후감) 데이터를 가져오면서, 해당 Review에 작성된 댓글 리스트를 가져오고 싶었다. 

spring-data-jpa로 구현하니, 다음과 같이 구현할 수 있었다.

@Transactional(readOnly = true)
public TmpReviewDetailResponse getReviewDetail(Long reviewId){
    Review review = reviewRepository.findByReviewId(reviewId);
    return ReviewDetailResponse.from(review);
}
@Getter
@Builder
@AllArgsConstructor
public class ReviewDetailResponse {
    private String reviewTitle;
    private String reviewContent;
    private String reviewImg;
    private String reviewerNickname;
    private String reviewerPhoto;
    private List<ReviewCommentResponse> reviewComments;

    public static ReviewDetailResponse from(Review review){
        ReviewDetailResponseBuilder builder = ReviewDetailResponse.builder()
                .reviewTitle(review.getReviewTitle())
                .reviewContent(review.getReviewContent())
                .reviewImg(review.getReviewImg())
                .reviewerNickname(review.getUser().getUserNickname());

        List<ReviewCommentResponse> reviewCommentResponses = new ArrayList<>();
        for (ReviewComment reviewComment : review.getReviewComment()) {
            ReviewCommentResponse commentResponse = ReviewCommentResponse.builder()
                    .commenterId(reviewComment.getUser().getUserId())
                    .commenterNickname(reviewComment.getUser().getUserNickname())
                    .commenterImg(reviewComment.getUser().getUserPhoto())
                    .commentText(reviewComment.getCommentText())
                    .build();
            reviewCommentResponses.add(commentResponse);
        }
        builder.reviewComments(reviewCommentResponses);
        return builder.build();
    }

}
@Getter
@Builder
public class ReviewCommentResponse {
    private Long commenterId;
    private String commenterNickname;
    private String commenterImg;
    private String commentText;
}

조회는 잘 되었지만, N+1 문제가 발생했다.

그래서 공부한대로 @Query의 fetch join을 사용하여 해결하고자 했다.

 

그러나, review 정보 하나에 reviewComment 리스트를 가져오는 것을 mysql 쿼리 작성하듯이 작성하기 어려웠다.

 

그래서 검색해보니, 게시글과 댓글 조회를 구현할 때 게시글 데이터 조회와 댓글 조회 api를 분리하는 것이 좋다고 한다. 

https://www.inflearn.com/questions/161001/api-%EC%84%A4%EA%B3%84%EC%8B%9C-%EA%B2%8C%EC%8B%9C%EA%B8%80%EA%B3%BC-%EB%8C%93%EA%B8%80

 

API 설계시 게시글과 댓글 - 인프런 | 질문 & 답변

안녕하세요 강사님.http, 스프링, jpa 모두 강사님 수업을 듣고 인생 첫 스프링 프로젝트로 게시판 API를 구현 해 보려고 하는 대학생입니다.그래서 현재 아래와 같이 요청경로와 요청법, 응답 본문

www.inflearn.com

 

하지만 나의 니즈는 그것이 아니었다. 

한 번에 조회하고 싶었다. (페이징을 적용할 거면 api 분리하는 게 좋겠지만, 페이징 요구사항은 없었기에 한 번에 조회하길 원했다.)

 

 

N+1 문제도 해결하고 한 번에 데이터를 조회하는 방법을 찾아봤다.

 

엔티티 상황

- Review

- ReviewComment

- User

 

Review 데이터 하나를 조회하는데

Review를 작성한 유저의 nickname을 User에서 가져와야 함.

ReviewComment에서 reviewId로 필터링한 리스트를 가져와야 함.

ReviewComment를 작성한 유저의 nickname을 User에서 가져와야 함.

 

다음과 같이..

{
    "reviewTitle": "독후감제목",
    "reviewContent": "블라블라 넘 재밌구용",
    "reviewImg": null,
    "reviewerNickname": "test1",
    "reviewerPhoto": null,
    "reviewComments": [
        {
            "commenterId": 1,
            "commenterNickname": "test1",
            "commenterImg": null,
            "commentText": "댓글달기 성공"
        },
        {
            "commenterId": 2,
            "commenterNickname": "test2",
            "commenterImg": null,
            "commentText": "댓글달기 미쳤다"
        }
    ]
}

 

 

다음과 같이 @QueryProjection을 작성한다. 

그리고 다시 컴파일해주면 QTmp... 가 생길 것이다. 

Tmp로 클래스명을 작성한 이유는 위와 겹치지 않기 위해서.. 
나중에 Tmp를 제거한 이름으로 고칠 것이다.
@Getter
public class TmpReviewDetailResponse {
    private String reviewTitle;
    private String reviewContent;
    private String reviewImg;
    private String reviewerNickname;
    private String reviewerPhoto;
    private List<TmpReviewDetailCommentResponse> reviewComments;

    public void setReviewComments(List<TmpReviewDetailCommentResponse> reviewComments) {
        this.reviewComments = reviewComments;
    }

    @QueryProjection
    public TmpReviewDetailResponse(String reviewTitle, String reviewContent, String reviewImg,
                                   String reviewerNickname, String reviewerPhoto){
        this.reviewTitle = reviewTitle;
        this.reviewContent = reviewContent;
        this.reviewImg = reviewImg;
        this.reviewerNickname = reviewerNickname;
        this.reviewerPhoto = reviewerPhoto;
    }
}
@Getter
public class TmpReviewDetailCommentResponse {
    private Long commenterId;
    private String commenterNickname;
    private String commenterImg;
    private String commentText;

    @QueryProjection
    public TmpReviewDetailCommentResponse(Long commenterId, String commenterNickname,
                                          String commenterImg, String commentText){
        this.commenterId = commenterId;
        this.commenterNickname = commenterNickname;
        this.commenterImg = commenterImg;
        this.commentText = commentText;
    }
}

 

@Transactional(readOnly = true)
public TmpReviewDetailResponse getReviewDetail(Long reviewId){
    TmpReviewDetailResponse response =queryFactory
                .select(new QTmpReviewDetailResponse(
                            review.reviewTitle,
                            review.reviewContent,
                            review.reviewImg,
                            user.userNickname,
                            user.userPhoto
                    ))
                .from(review)
                .innerJoin(review.user, user)
                .where(review.reviewId.eq(reviewId))
                .fetchOne();
        List<TmpReviewDetailCommentResponse> comments = queryFactory
                .select(new QTmpReviewDetailCommentResponse(
                        user.userId,
                        user.userNickname,
                        user.userPhoto,
                        reviewComment.commentText
                ))
                .from(reviewComment)
                .innerJoin(reviewComment.user, user)
                .where(reviewComment.review.reviewId.eq(reviewId))
                .fetch();

        response.setReviewComments(comments);
        return response;

}

 

 

이렇게 실행하면 다음 두 쿼리만 나간다. 

이전에는 review 쿼리 & review 작성한 유저 쿼리 & 댓글 수만큼의 작성한 유저 쿼리가 나갔다.

select
    r1_0.r_title,
    r1_0.r_content,
    r1_0.r_picture,
    u1_0.u_nickname,
    u1_0.u_photo 
from
    review r1_0 
join
    user u1_0 
        on u1_0.u_id=r1_0.u_id 
where
    r1_0.r_id=?


select
    r1_0.u_id,
    u1_0.u_nickname,
    u1_0.u_photo,
    r1_0.comment_text 
from
    review_comment r1_0 
join
    user u1_0 
        on u1_0.u_id=r1_0.u_id 
where
    r1_0.r_id=?

 


https://velog.io/@do-hoon/JPA-QueryDSL-%EA%B3%84%EC%B8%B5%ED%98%95-%EB%8C%93%EA%B8%80-%EB%8C%80%EB%8C%93%EA%B8%80-%EA%B5%AC%ED%98%842

 

JPA + QueryDSL 계층형 댓글, 대댓글 구현(2)

이번엔 전편에 이어서 계층형 댓글, 대댓글을 다시 리팩토링해볼 예정이다. 이전 게시글에서는 계층형 댓글, 대댓글을 구현은 되었지만 N+1 문제가 있었다. 이번에는 그 N+1 문제를 해결해 볼 것

velog.io

 

댓글