게시글의 댓글을 어떻게 불러와야 할까? - N+1 문제의 늪(1)
게시글에 달린 댓글을 조회하는 기능을 구현하게 되었다.
단순히 생각하면 별 것 아닌 기능처럼 보이지만, 막상 구현에 들어가 보니 생각보다 고민할 부분이 많았다.
왜냐하면 "대댓글" 때문이다.
현재 내 목표는 다음과 같다
클라이언트(프론트)에게 댓글 리스트를 줄 때,
각 댓글이 부모 댓글인지, 대댓글인지를 구분해서 내려주는 것.
아래처럼 말이다
[
{
"id": 1,
"content": "부모 댓글",
"children": [
{
"id": 2,
"content": "대댓글"
}
]
}
]
처음에는 내가 너무 순진했다.
단순히 댓글을 Post 기준으로 전부 불러오고 createdAt 기준으로 최신순 정렬만 하면 끝이라고 생각했다.
그래서 Repository를 JPA를 사용하여 아래처럼 간단하게 작성했다.
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findAllByPostOrderByCreatedAtDesc(Post post);
}
그리고 서비스에선 이렇게 간단하고 깔끔하게 사용하였다.
List<Comment> comments = commentRepository.findAllByPostOrderByCreatedAtDesc(post);
이후 댓글을 가져온 후, 각 댓글의 작성자 이름을 표시해야 해서 아래와 같이 comment.getUser().getNickname()을 호출했다.
for (Comment comment : comments) {
String nickname = comment.getUser().getNickname(); // 여기가 뭔가 수상하다...
}
여기서 의심이 시작됐다.
댓글마다 유저 정보를 가져올것이고 나는 LAZY로 설정을 했는데
그럼 댓글 수 만큼 쿼리가 나가는 거 아닌가?

내 의심이 맞았다. 쿼리가 댓글 수 만큼 추가로 실행되고 N+1 문제가 터졌다.
내 Comment Entity는 다음과 같이 User, Post, ParentComment와 연관관계를 가지고 있고
모두 LAZY 타입으로 정의되어있다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_comment_id")
private Comment parentComment;
따라서, 위에서 댓글을 100개 가져오고 getUser()을 호출하면
Repository에서 쿼리 한 번
SELECT * FROM comments WHERE post_id = ?
SELECT * FROM users WHERE user_id = ? ← 최대 100번 (댓글 수만큼)
이렇게 해서 총 1 + N번의 쿼리 (N+1 문제)가 발생하게 된다.
근데 여기서 더 큰 문제는 또 있다 ^_^
내 경우에는 user만의 문제가 아니라 대댓글까지 있는 계층적 구조이다.
댓글 -> 부모가 없는 최상위 댓글
대댓글 -> 부모 댓글이 있는 댓글
따라서 .getParentComment()를 만약 호출했다면 또 다른 쿼리가 날아간다.
끔찍하죠??

그래서 정리하자면 처음에 내가 작성한 코드는 간단해 보이지만 내부에 엄청난 문제가 있는 코드였다.
(사실 데이터가 적은 사이드 프로젝트에선 괜찮다 ㅎㅎ)
이제 다음 글에선 이 문제를 해결하기 위해 내가 선택했던 방법과 코드를 설명하겠다!