대댓글 페이지네이션

Computer Science/Spring Boot

이번 프로젝트에서 댓글-대댓글을 한꺼번에 페이징해야할 일이 생겼다. 한 번 쿼리문을 작성해봅시다...!

 

테이블 구성은 다음과 같다.

Comment Entity

어디서나 흔하게 볼 수 있는 댓글의 엔티티 구조! 처음엔 대댓글을 가져올 때 편하도록 양방향 매핑을 할까 고민을 했으나 성능을 고려하여 단방향 관계로 남겨두었다. 추가로 요구사항은 댓글의 최대 깊이를 1로 제한하고 있고 모바일 앱 기준으로 무한 스크롤 방식의 페이지네이션을 설계하려고 한다.

 

이제 한 번의 쿼리문으로 이 엔티티에서 댓글-대댓글을 페이지네이션을 하려고 하는데... 어떻게 쿼리문을 구성해야할지 열심히 고민하던 도중 아래 블로그에서 힌트를 얻을 수 있었다.

 

https://velog.io/@lsvk9921/%EB%8C%93%EA%B8%80%EB%8C%80%EB%8C%93%EA%B8%80-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

 

댓글,대댓글 페이지네이션 구현하기

프로젝트 하던중에 게시판을 만들어야 했다post 테이블과 postComment 테이블이 있고Comment 는 다음과 같다댓글 그리고 대댓글만 제공하기로 하였고replyCommentId는 값이 없으면 부모댓글(depth=0)값이 있

velog.io

 

1. 시간 순(id 기준) 페이지네이션

FROM community_comments p
LEFT JOIN community_comments r
  ON r.parent_id = p.comment_id

간단하게 생각해보면 부모 댓글의 pk와 자식 댓글의 fk를 JOIN하여 쿼리하는 방식이 있다. 하지만 우리가 원하는건 페이지네이션! 대댓글도 날짜별로 정렬하려면 추가적인 정보가 필요하다. 단순하게 시간 순(id 기준)으로 페이지네이션을 진행한다고 했을 때 부모 댓글 A의 자식 댓글이 부모 댓글 B 보다 늦게 달리고 이미 부모 댓글 B까지 응답이 나갔다면 A의 자식 댓글은 누락되는 문제가 발생한다. 마지막으로 본 댓글만으로 페이지네이션을 할 경우 올바르게 구현하기 힘들다.

 

* LEFT JOIN을 사용하는 이유: 자식 댓글이 없는 부모 댓글도 포함해야하기 때문!

 

2. 기준을 두 개를 잡자!

부모 댓글 - 자식 댓글을 JOIN 후 마지막으로 본 부모 댓글, 자식 댓글을 모두 받기!

AND (
      (:primaryOffset IS NULL)                                           -- 조건 1
   OR ( p.comment_id > :primaryOffset )                                 -- 조건 2  
   OR ( p.comment_id = :primaryOffset AND r.comment_id > COALESCE(:subOffset, 0) )  -- 조건 3
    )

조건1: 첫 페이지 요청시 사용 (primaryOffset이 NULL이면 모든 댓글 조회 시작)

조건2: 이전에 본 마지막 부모 댓글 이후의 새로운 부모 댓글들 (완전히 새로운 부모 댓글 그룹으로 넘어가는 경우)

조건3: 같은 부모 댓글 내에서 아직 보지 않은 자식 댓글들

* COALESCE는 매개변수를 왼쪽부터 확인해서 NULL이 아닌 첫 번째 값을 반환하는 함수

 

해당 조건으로 쿼리한다면 정상적으로 부모 댓글 - 자식 댓글을 페이지네이션 할 수 있다!

 

결과 쿼리문

@Query(value = """
    SELECT
      p.관련한 필드,
      r.관련한 필드
    FROM community_comments p
      LEFT JOIN community_comments r
        ON r.parent_id = p.comment_id
        AND r.deleted_at IS NULL
      LEFT JOIN users pu
        ON pu.uid = p.user_id
      LEFT JOIN users cu
        ON cu.uid = r.user_id
    WHERE p.post_id        = :postId
      AND p.parent_id      IS NULL
      AND p.deleted_at     IS NULL
      AND (
            (:primaryOffset IS NULL)
         OR ( p.comment_id > :primaryOffset )
         OR ( p.comment_id = :primaryOffset AND r.comment_id > COALESCE(:subOffset, 0) )
          )
    ORDER BY p.comment_id ASC, r.comment_id ASC
    LIMIT :limit
    """, nativeQuery = true
    )
    List<CommunityCommentDTO.CommentCursorProjection> findCommentByCursor(
            @Param("postId")        Long postId,
            @Param("primaryOffset") Long primaryOffset,  // 마지막으로 본 parent.comment_id
            @Param("subOffset")     Long subOffset,      // 동일 parent 아래 마지막으로 본 child.comment_id
            @Param("limit")         int   limit          // 페이지 크기
    );

유저 정보를 조회해야하기 때문에 조인이 정말 많이 들어간 괴랄한 쿼리문이 완성되었다...

 

아직 미숙하기 때문에 혹시 왜 이렇게 쿼리문을 구성했지 하는 부분, 개선할 사항이나 피드백은 언제나 환영입니다! 편하게 댓글 주세요!!

'Computer Science > Spring Boot' 카테고리의 다른 글

JPA N+1 문제  (0) 2025.09.02
Spring MVC와 Servlet  (0) 2025.08.29
스프링 컨테이너와 빈(Bean) - Lifecycle Callback과 Scope  (0) 2025.08.24
스프링 컨테이너와 빈(Bean) - 싱글톤과 의존관계 주입 방법  (0) 2025.08.21
스프링 컨테이너와 빈(Bean) - ApplicationContext 동작 과정  (0) 2025.08.17
'Computer Science/Spring Boot' 카테고리의 다른 글
  • Spring MVC와 Servlet
  • 스프링 컨테이너와 빈(Bean) - Lifecycle Callback과 Scope
  • 스프링 컨테이너와 빈(Bean) - 싱글톤과 의존관계 주입 방법
  • 스프링 컨테이너와 빈(Bean) - ApplicationContext 동작 과정
hojoo
hojoo
그냥 개발이 즐거운 사람
  • hojoo
    dev_record
    hojoo
  • 전체
    오늘
    어제
    • 분류 전체보기 (82)
      • Study (0)
        • 모든 개발자를 위한 HTTP 웹 기본 지식 (0)
        • Real MySQL 8.0 (0)
        • 친절한 SQL 튜닝 (0)
        • 도메인 주도 개발 시작하기 (0)
        • 대규모 시스템 설계 기초 (0)
      • Computer Science (66)
        • Problem Solving (30)
        • Data Structure (4)
        • Spring Boot (13)
        • DB (1)
        • Java (4)
        • OS (3)
        • Server (2)
        • Tech (0)
      • Security (16)
        • Reversing (15)
        • Assembly (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Reversing
    레나 튜토리얼
    21278
    서버 증설 횟수
    Header
    프로그래머스
    servlet
    2539
    Spring boot
    자료구조
    16946
    bean
    12033
    15973
    소수상근수
    DP
    x64dbg
    백준
    PE header
    n^2 배열 자르기
    리버싱 핵심원리
    리버싱
    DB
    13265
    HTTP
    9421
    Lena tutorial
    n+1
    dreamhack.io
    19622
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
hojoo
대댓글 페이지네이션
상단으로

티스토리툴바