/**
* 게시글의 모든 댓글(대댓글 포함) 조회
* - 최상위 댓글만 조회 → 각 댓글의 자식 목록(childComments)을 재귀적으로 DTO 변환
*/
public List<CommentDTO> getAllCommentsByPostId(Long postId) {
// 1) 최상위 댓글들 (parentComment = null)
List<Comment> topComments = commentRepository.findByPost_PostIdAndParentCommentIsNull(postId);
// 2) 재귀적으로 childComments까지 DTO로 변환
List<CommentDTO> result = new ArrayList<>();
for (Comment comment : topComments) {
CommentDTO dto = convertToDTOWithChildren(comment);
result.add(dto);
}
return result;
}
// 자식 댓글까지 재귀 변환
private CommentDTO convertToDTOWithChildren(Comment comment) {
CommentDTO dto = CommentDTO.builder()
.commentId(comment.getCommentId())
.userId(comment.getUserId())
.content(comment.getContent())
.createdAt(comment.getCreatedAt())
.updatedAt(comment.getUpdatedAt())
.postId(comment.getPost().getPostId())
.depth(comment.getDepth())
.parentCommentId(comment.getParentComment() == null
? null : comment.getParentComment().getCommentId())
.build();
// childComments
List<CommentDTO> childDtos = new ArrayList<>();
for (Comment child : comment.getChildComments()) {
childDtos.add(convertToDTOWithChildren(child));
}
dto.setChildComments(childDtos);
return dto;
}
우선 코드를 보다가 성능에 문제가 있을 것이라고 생각되는 부분을 발견해서 수정을 진행했다.
원래 코드에서는 최상위 댓글을 가져온 후, 각 댓글의 childComments를 재귀적으로 조회했다.
즉, 최상위 댓글을 가져오는 첫 번째 쿼리 이후, 각 최상위 댓글마다 자식 댓글을 조회하는 추가적인 N개의 쿼리가 실행된 것이다.
이를 N+1 문제(N+1 problem)가 발생했다고 한다. 조회된 엔티티의 개수만큼 연관된 엔티티를 조회하기 위해 추가적인 쿼리가 발생하는 문제이다.
1번의 findByPost_PostIdAndParentCommentIsNull(postId) 를 실행한 경우 N개의 comment.getChildComments()가 실행되니 최악의 경우 O(N) 쿼리가 실행되는 것이다.
1. N+1 쿼리 문제 해결
public interface CommentRepository extends JpaRepository<Comment, Long> {
// (1) 특정 게시글 + 부모가 없는(최상위) 댓글 조회
List<Comment> findByPost_PostIdAndParentCommentIsNull(Long postId);
// 필요하다면, 특정 부모 댓글의 자식 조회
List<Comment> findByParentComment_CommentId(Long parentId);
// 추가한 메서드(N+1 쿼리 문제 해결)
@Query("SELECT c FROM Comment c LEFT JOIN FETCH c.childComments WHERE c.post.postId = :postId")
List<Comment> findAllCommentsWithChildrenByPostId(Long postId);
}
일단 위는 CommentRepository의 풀 코드이다.
기존 코드의 방식을 보면, 아래와 같다.
List<Comment> topComments = commentRepository.findByPost_PostIdAndParentCommentIsNull(postId);
for (Comment comment : topComments) {
comment.getChildComments(); // 이 부분에서 추가 쿼리 실행 (N번)
}
위의 문제를 해결하기 위해서,
N+1 쿼리 문제를 해결하기 위해서 CommentRepository에서 아래와 같이 JPQL을 이용해 JOIN FETCH를 적용했다.
@Query("SELECT c FROM Comment c LEFT JOIN FETCH c.childComments WHERE c.post.postId = :postId")
List<Comment> findAllCommentsWithChildrenByPostId(Long postId);
(부연 설명)
LEFT JOIN FETCH c.childComments:
- 댓글을 가져올 때 자식 댓글까지 한 번에 로딩
- 즉시 로딩을 적용하여 단일 쿼리로 모든 댓글과 자식 댓글을 가져오도록 함
WHERE c.post.postId = :postId: 특정 게시글의 댓글만 조회
public List<CommentDTO> getAllCommentsByPostId(Long postId) {
List<Comment> allComments = commentRepository.findAllCommentsWithChildrenByPostId(postId);
Map<Long, CommentDTO> dtoMap = new HashMap<>();
// 1차 변환: 모든 댓글을 DTO로 변환하여 Map에 저장
allComments.forEach(comment -> {
CommentDTO dto = convertToDTO(comment);
dtoMap.put(dto.getCommentId(), dto);
});
// 2차 변환: 계층 구조를 구성 (부모-자식 관계 정리)
List<CommentDTO> result = new ArrayList<>();
dtoMap.values().forEach(dto -> {
if (dto.getParentCommentId() == null) { // 최상위 댓글이면 리스트에 추가
result.add(dto);
} else { // 대댓글이면 부모의 childComments 리스트에 추가
CommentDTO parent = dtoMap.get(dto.getParentCommentId());
parent.getChildComments().add(dto);
}
});
return result;
}
변경된 getAllCommentsByPostId의 로직을 분석해보면,
앞서 JOIN FETCH를 적용한 findAllCommentsWithChildrenByPostId(postId)를 통해 한번의 SQL 실행으로 모든 댓글과 대댓글을 가져오도록 했다. 또한 Map 자료구조를 사용해서 모든 댓글을 DTO로 변환 후 Map<Long, CommentDTO>에 저장시켜 빠른 탐색이 가능하도록 수정했다. 그리고 dtoMap을 이용해서 부모 댓글에 자식 댓글을 추가하는 방식으로 변경했다. 기존의 재귀 호출을 제거하여 스택 오버플로우를 방지하고 성능 개선을 이루어냈다. 실행 쿼리수도 1번으로 떨어지고, 느린 재귀 호출방식을 피하여 빠른 성능을 이끌어내었다.
'Project' 카테고리의 다른 글
| [여정 프로젝트] 메인 페이지 로딩 25초 → 1.5초, 최적화 (0) | 2025.06.14 |
|---|