시나리오
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를 분리하는 것이 좋다고 한다.
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=?
JPA + QueryDSL 계층형 댓글, 대댓글 구현(2)
이번엔 전편에 이어서 계층형 댓글, 대댓글을 다시 리팩토링해볼 예정이다. 이전 게시글에서는 계층형 댓글, 대댓글을 구현은 되었지만 N+1 문제가 있었다. 이번에는 그 N+1 문제를 해결해 볼 것
velog.io
'개발 > Spring' 카테고리의 다른 글
[N+1 문제] 일반조인, fetch조인으로 OSIV, LAZY/EAGER, N+1문제 확실히 이해하기 (0) | 2023.08.16 |
---|---|
JPA 프록시 개념/존재이유와 find(), getReferenceById() (0) | 2023.08.08 |
N+1 문제란? feat. 해결방법 - 개념적 이야기 (0) | 2023.08.06 |
QueryDsl을 이용한 No offset 구현하기 (0) | 2023.08.06 |
싱글톤 LazyHolder 적용해보기 (0) | 2023.08.03 |
댓글