댓글/덧글 N+1 문제 해결하기

ideafy·2025년 11월 11일

프로젝트

목록 보기
21/25

문제 상황


게시물 상세보기를 하면 위와 댓글을 조회하는 API가 호출된다.
하지만 N+1 문제가 발생했다

기존 댓글 조회 메서드

public List<CommentResponse> getCommentList(Long ideaPostId) {
        List<Comment> commentList = commentRepository.findAllByIdeaPostId(ideaPostId);
        return commentList.stream().map(CommentResponse::new).toList();
    }
  1. DB에서 post id에 해당하는 모든 댓글 엔티티를 조회한다.
  2. 댓글 엔티티들을 CommentResponse DTO로 변환하고 응답한다.

문제는 엔티티를 DTO로 변환할 때 발생한다.

기존 댓글 응답 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());
    }
}

기존 DTO의 문제점

  1. 댓글 엔티티를 그대로 DTO로 가져와서 user의 정보를 조회한다.
    -> + user 조회 쿼리 1회
  2. 댓글 엔티티에서 자식 댓글을 순회하면서 DTO로 변환한다.
    -> + 자식 댓글 조회 쿼리 n회
    n = 전체 댓글 수 + 자식이 있는 댓글 수

해결

개선 댓글 조회 메서드(repository)

@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);
  1. inner join으로 user 정보까지 한번에 조회한다.
  2. 게시글의 모든 댓글을 필요한 데이터만 flat하게 조회한다.

개선 댓글 조회 메서드(service)

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();
    }
  1. 계층화를 위해 Map을 사용: 자식이 부모를 효율적으로 찾을 수 있게 하기 위함.
    • LinkedHashMap을 사용해 댓글 오름차순 정렬을 유지한다.
  2. flat하게 조회한 댓글을 계층화 처리해준다.

개선 댓글 응답 DTO

@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

Q&A

Q1. 댓글을 flat하게 조회하면 계층화 처리할 때 부모랑 자식이랑 섞여서 부정확해지는 거 아닌가요?

A1. 댓글 조회 시 오름차순 정렬로 이를 해결했습니다. 자식 댓글은 부모 댓글보다 생성일자가 늦을 수밖에 없고, 계층화 처리 시 Map을 사용해 부모에 추가해주기 때문에 문제 없습니다.


Q2. @EntityGraph 나 Fetch Join 안 쓰시나요?

A2. 현재 구조(1층 트리)에서는 사용할 수 있지만 추후에 재귀적 트리 구조의 댓글 조회를 구현하기 위해서는 flat 조회 + 계층화가 필요하다고 생각해 확장성을 위해 flat 조회 + 계층화 구조로 구현했습니다.

profile
재밌게 공부하고 싶어요

0개의 댓글