
게시물 상세보기를 하면 위와 댓글을 조회하는 API가 호출된다.
하지만 N+1 문제가 발생했다
public List<CommentResponse> getCommentList(Long ideaPostId) {
List<Comment> commentList = commentRepository.findAllByIdeaPostId(ideaPostId);
return commentList.stream().map(CommentResponse::new).toList();
}
문제는 엔티티를 DTO로 변환할 때 발생한다.
public record CommentResponse(
Long id,
String content,
ZonedDateTime createdAt,
CommentUserResponse user,
List<CommentResponse> childComments
) {
public CommentResponse(Comment comment) {
this(
comment.getId(),
comment.getContent(),
comment.getCreatedAt(),
new CommentUserResponse(comment.getUser()),
comment.getChildComments() == null ? null : comment.getChildComments().stream().map(CommentResponse::new).toList()
);
}
}
record CommentUserResponse(
Long userId,
String username,
String profileImageUrl
) {
CommentUserResponse(User user) {
this(user.getId(), user.getUsername(), user.getProfileImageUrl());
}
}
@Query("SELECT new com.kth.softlaunchers.dto.comment.CommentResponse2(c.id, c.content, c.createdAt, c.parentComment.id, u.id, u.username, u.profileImageUrl) " +
"FROM Comment c " +
"INNER JOIN users u ON u.id = c.user.id " +
"WHERE c.ideaPost.id = :ideaPostId AND c.isDeleted IS FALSE ORDER BY c.createdAt ASC ")
List<CommentResponse2> findAllByIdeaPostId2(@Param("ideaPostId") Long ideaPostId);
public List<CommentResponse2> getCommentList2(Long ideaPostId) {
List<CommentResponse2> commentList = commentRepository.findAllByIdeaPostId2(ideaPostId);
Map<Long, CommentResponse2> commentMap = new LinkedHashMap<>();
for(CommentResponse2 comment: commentList) {
Long parentId = comment.getParentId();
if (parentId == null) { // 자신이 부모 comment 이면
commentMap.put(comment.getId(), comment);
} else { // 부모 comment 가 있으면 자식 comment 로 추가
CommentResponse2 parent = commentMap.get(parentId);
parent.addChild(comment);
}
}
return commentMap.values().stream().toList();
}
@Getter
public class CommentResponse2 {
private final Long id;
private final String content;
private final ZonedDateTime createdAt;
private final Long parentId;
private final CommentUserResponse2 user;
private List<CommentResponse2> childComments;
public CommentResponse2(Long id, String content, ZonedDateTime createdAt, Long parentId,
Long userId, String username, String profileImageUrl) {
this.id = id;
this.content = content;
this.createdAt = createdAt;
this.parentId = parentId;
this.user = new CommentUserResponse2(userId, username, profileImageUrl);
}
public void addChild(CommentResponse2 child) {
if (childComments == null) {
childComments = new ArrayList<>();
}
childComments.add(child);
}
}
record CommentUserResponse2(Long userId, String username, String profileImageUrl) {
}
개선 후 1번의 쿼리로 모든 댓글을 조회할 수 있게 됐다.
select
c1_0.id,
c1_0.content,
c1_0.created_at,
c1_0.parent_comment_id,
u1_0.id,
u1_0.username,
u1_0.profile_image_url
from
comment c1_0
join
users u1_0
on u1_0.id=c1_0.user_id
where
c1_0.idea_post_id=?
and c1_0.is_deleted is false
order by
c1_0.created_at
Q1. 댓글을 flat하게 조회하면 계층화 처리할 때 부모랑 자식이랑 섞여서 부정확해지는 거 아닌가요?
A1. 댓글 조회 시 오름차순 정렬로 이를 해결했습니다. 자식 댓글은 부모 댓글보다 생성일자가 늦을 수밖에 없고, 계층화 처리 시 Map을 사용해 부모에 추가해주기 때문에 문제 없습니다.
Q2. @EntityGraph 나 Fetch Join 안 쓰시나요?
A2. 현재 구조(1층 트리)에서는 사용할 수 있지만 추후에 재귀적 트리 구조의 댓글 조회를 구현하기 위해서는 flat 조회 + 계층화가 필요하다고 생각해 확장성을 위해 flat 조회 + 계층화 구조로 구현했습니다.