이번엔 전편에 이어서 계층형 댓글, 대댓글을 다시 리팩토링해볼 예정이다.
@Transactional
public PostOneResponse getOnePost(Long postId) {
PostOneResponse postOneResponse = postRepository.findOnePostById(postId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_POST));
commentsExtractor(postId, postOneResponse);
return postOneResponse;
}
private void commentsExtractor(Long postId, PostOneResponse postOneResponse) {
postOneResponse.getComments()
.forEach(comment -> {
List<CommentsChildrenResponse> comments = commentRepository.findPostComments(postId, comment.getCommentId());
comment.setChildren(comments);
});
}
위의 로직은 이 전편에서 만들었던 N+1
문제가 발생하던 로직이다.
만약 부모 댓글의 개수가 100개
라면 100번
이상의 쿼리가 나가게 되는 아주 좋지 못한 코드이다..ㅠ 🥲
- QueryDSL 코드
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostCustomRepository {
private final JPAQueryFactory queryFactory;
@Override
public Optional<PostOneResponse> findOnePostById(Long postId, Long userId) {
queryFactory.update(post)
.set(post.viewCount, post.viewCount.add(1))
.where(post.id.eq(postId))
.execute();
Optional<PostOneResponse> response = Optional.ofNullable(queryFactory
.select(new QPostOneResponse(
post.id,
post.title,
post.content,
post.scraps.size(),
post.comments.size(),
post.postLikes.size(),
post.timeEntity.createdDate,
post.timeEntity.updatedDate,
post.viewCount,
user.nickname,
JPAExpressions
.selectFrom(post)
.where(user.id.eq(userId))
.exists(),
JPAExpressions
.selectFrom(postLike)
.where(postLike.post.eq(post).and(user.id.eq(userId)))
.exists(),
JPAExpressions
.selectFrom(scrap)
.where(scrap.post.eq(post).and(user.id.eq(userId)))
.exists()))
.from(post)
.innerJoin(post.user, user)
.where(post.id.eq(postId))
.fetchOne());
if (response.isEmpty()) {
return Optional.empty();
}
List<PostOneCommentResponse> comments = queryFactory
.select(new QPostOneCommentResponse(
comment.parent.id,
comment.id,
comment.content,
user.nickname,
JPAExpressions
.selectFrom(comment)
.where(user.id.eq(userId))
.exists(),
comment.timeEntity.createdDate,
comment.timeEntity.updatedDate))
.from(comment)
.innerJoin(comment.post, post)
.innerJoin(comment.user, user)
.where(post.id.eq(postId).and(comment.parent.id.isNull()))
.orderBy(comment.id.asc())
.fetch();
List<CommentsChildrenResponse> childComments = queryFactory
.select(new QCommentsChildrenResponse(
comment.parent.id,
comment.id,
comment.content,
user.nickname,
JPAExpressions
.selectFrom(comment)
.where(user.id.eq(userId))
.exists(),
comment.timeEntity.createdDate,
comment.timeEntity.updatedDate
))
.from(comment)
.innerJoin(comment.post, post)
.innerJoin(comment.user, user)
.where(post.id.eq(postId).and(comment.parent.id.isNotNull()))
.fetch();
comments.stream()
.forEach(parent -> {
parent.setChildren(childComments.stream()
.filter(child -> child.getParentId().equals(parent.getCommentId()))
.collect(Collectors.toList()));
});
response.get().setComments(comments);
return response;
}
}
위의 코드는 게시글을 조회
할 때 댓글
+ 대댓글
을 한번에 다 가져와서 JSON
을 계층형으로 만들어주는 쿼리이다.
이 전편에 비해서 코드가 많이 바뀌었다. 😁
간략히 위의 코드를 설명하자면 게시글 조회 로직이 실행되는 순간 viewCount
가 1이 증가하는 쿼리가 나가고, post
를 가져오는 조회 쿼리가 나가게 된다.
JPAExpressions
은 서브쿼리로 사용을 하였는데 이것은 boolean
타입으로 반환이 되며 사용자의 게시글, 좋아요 여부, 스크랩 여부를 확인하는 QueryDSL
에서 지원하는 기능이다.
post
가 만약 없다면 Optional
이 return이 되고, 존재한다면 아래 로직을 실행한다. 댓글을 가져오는 방법이 여러가지가 있는데 저는 부모 댓글을 먼저 가져왔습니다. (parentId = null)
부모 댓글을 가져온 후 자식 댓글을 가져온다. (parentId = notNull)
이렇게 가져온 후 부모 댓글이 List
로 반환이 되고 stream
을 사용해서 commentId
와 자식 댓글의 parentId
를 비교해서 같은 값이 있다면 List 타입으로 넣어주게 된다.
이렇게 총 4번의 쿼리가 실행이 된다. (update 1번, select 3번)
- PostOneResponse
@ApiModel(description = "결과 응답 데이터 모델")
@Getter
@Setter
@NoArgsConstructor
public class PostOneResponse {
@ApiModelProperty(value = "게시글 Id")
private Long postId;
@ApiModelProperty(value = "게시글 제목")
private String title;
@ApiModelProperty(value = "게시글 내용")
private String content;
@ApiModelProperty(value = "해당 게시글의 전체 스크랩 수")
private int scrapCount;
@ApiModelProperty(value = "해당 게시글의 전체 댓글의 수")
private int commentCount;
@ApiModelProperty(value = "해당 게시글의 전체 좋아요 수")
private int likeCount;
@ApiModelProperty(value = "해당 게시글의 생성 시간")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createDate;
@ApiModelProperty(value = "해당 게시글의 수정 시간")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime updateDate;
@ApiModelProperty(value = "해당 게시글의 조회수")
private Integer viewCount;
@ApiModelProperty(value = "해당 게시글의 생성 회원 이름")
private String username;
@ApiModelProperty(value = "해당 게시글의 유저 본인 확인")
private boolean myPost;
@ApiModelProperty(value = "해당 게시글의 좋아요 본인 확인")
private boolean myLike;
@ApiModelProperty(value = "해당 게시글의 스크랩 본인 확인")
private boolean myScrap;
@ApiModelProperty(value = "해당 게시글의 댓글")
private List<PostOneCommentResponse> comments = new ArrayList<>();
@QueryProjection
public PostOneResponse(Long postId, String title, String content, int scrapCount, int commentCount, int likeCount, LocalDateTime createDate, LocalDateTime updateDate, Integer viewCount, String username, boolean myPost, boolean myLike, boolean myScrap) {
this.postId = postId;
this.title = title;
this.content = content;
this.scrapCount = scrapCount;
this.likeCount = likeCount;
this.commentCount = commentCount;
this.createDate = createDate;
this.updateDate = updateDate;
this.viewCount = viewCount;
this.username = username;
this.myPost = myPost;
this.myLike = myLike;
this.myScrap = myScrap;
}
}
- postOneCommentResponse
@Getter
@Setter
@NoArgsConstructor
public class PostOneCommentResponse {
private Long parentId;
private Long commentId;
private String content;
private String username;
private boolean myComment;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createDate;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime updateDate;
private List<CommentsChildrenResponse> children = new ArrayList<>();
@QueryProjection
public PostOneCommentResponse(Long parentId, Long commentId, String content, String username, boolean myComment, LocalDateTime createDate, LocalDateTime updateDate) {
this.parentId = parentId;
this.commentId = commentId;
this.content = content;
this.username = username;
this.myComment = myComment;
this.createDate = createDate;
this.updateDate = updateDate;
}
}
- CommentsChildrenResponse
@Getter
@Setter
@NoArgsConstructor
public class CommentsChildrenResponse {
private Long parentId;
private Long commentId;
private String content;
private String username;
private boolean myComment;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createDate;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime updateDate;
@QueryProjection
public CommentsChildrenResponse(Long parentId, Long commentId, String content, String username, boolean myComment, LocalDateTime createDate, LocalDateTime updateDate) {
this.parentId = parentId;
this.commentId = commentId;
this.content = content;
this.username = username;
this.myComment = myComment;
this.createDate = createDate;
this.updateDate = updateDate;
}
}
- 결과화면 (Postman)
N+1 문제는 완전히 사라졌고 나름 전에 비해 깔끔하게 리팩토링이 된것같다!!.. 🏡
내가 만든 방식 외에도 계층형 쿼리를 만드는 방법은 여러가지가 있는걸로 아는데 처음 만들어보는 계층형 쿼리이다보니 좀 어렵게 느껴졌던 것 같다.
다음 편은 게시글 + 파일 업로드편(AWS S3
) 만들어볼 예정이다.