현재 모든 게시글을 불러올 때, 댓글 개수를 따로 조회하고 합치는 방식으로 구현되어 있음.
이 방식의 문제점과 해결 방법을 살펴보자.
1. 기존 방식의 문제점
현재는 게시글을 먼저 불러오고, 댓글 개수를 따로 조회하여 합치는 방식을 사용하고 있다.
✅ 기존 코드
@Query("SELECT p FROM Post p " +
"LEFT JOIN FETCH p.user " +
"WHERE (:cursor IS NULL OR p.id < :cursor) " +
"ORDER BY p.id DESC " +
"LIMIT :limit")
List<Post> findAllPostsByCursor(@Param("cursor") Long cursor, @Param("limit") int limit);
@Query("SELECT c.post.id, COUNT(c) FROM Comment c WHERE c.post.id IN :postIds GROUP BY c.post.id")
List<Object[]> findCommentCountsByPostIds(@Param("postIds") List<Long> postIds);
이 방식의 동작 방식
- findAllPostsByCursor(cursor, limit) → 페이지네이션된 게시글 리스트 조회
- findCommentCountsByPostIds(postIds) → 해당 게시글들의 댓글 개수를 별도 조회
- 두 결과를 매핑하여 각 게시글에 댓글 개수를 합쳐서 반환
문제점
- 댓글 개수를 동적으로 불러와야 하기 때문에, 댓글이 추가/삭제될 경우 Cursor 정렬이 어긋날 가능성이 있음.
- 예시(좋아요 50개 이상(필터) + 댓글 많은 순(정렬)) 기준으로 게시글을 가져오는 경우 정합성이 깨질 수 있음.
- 추가적인 쿼리 실행이 필요하여 성능 부담이 증가함.
2. 해결 방법: Post 테이블에 comment_count 필드 추가
댓글 개수를 Post 테이블에 저장하고 댓글이 추가/삭제될 때 comment_count 필드를 업데이트하면,
게시글을 조회할 때 추가적인 쿼리 없이 댓글 개수를 가져올 수 있다.
추가적으로, 예시(좋아요 50개 이상 + 댓글 많은 순) 기준으로 게시글 가져오는 경우에도, Cursor 기반 정렬을 유지할 수 있다.
3. Post 엔티티 수정 (댓글 개수 필드 추가)
Post 엔티티 변경
@Column(name = "comment_counts")
@ColumnDefault(value = "0")
@Builder.Default
private Long commentCounts = 0L;
...
public void increaseCommentCounts() {
commentCounts++;
}
public void decreaseCommentCounts() {
if (commentCounts > 0) {
commentCounts--;
}
}
이 필드를 추가하면 Post 테이블에서 댓글 개수를 즉시 조회 가능
Post 엔티티에 필드 추가 및 메소드 생성
이제 댓글이 추가되면 increaseCommentCounts()가 호출되고, 삭제되면 decreaseCommentCounts()가 호출됨.
public CommentResponse create(Long userId, Long postId, String content, Long parentCommentId) {
...
post.increaseCommentCounts();
return CommentResponse.from(commentRepository.save(comment));
}
public void delete(Long userId, Long commentId) {
...
post.increaseCommentCounts();
commentRepository.deleteById(commentId);
}
6. PostService 리팩토링 (더 깔끔해진 버전)
이제 댓글 개수를 별도 쿼리로 불러올 필요 없이, 바로 Post 테이블에서 가져올 수 있음.
Offset 기반 페이지네이션
@Transactional(readOnly = true)
public Page<PostSummaryResponse> getAllPostsByPage(Pageable pageable) {
return postRepository.findAllPostSummariesByPage(pageable)
.map(PostSummaryResponse::from);
}
Cursor 기반 페이지네이션 (최적화된 방식)
@Transactional(readOnly = true)
public CursorResponse<PostSummaryResponse> getAllPostsByCursor(Long cursor, int size) {
// 커서 기반 조회
List<Post> posts = postRepository.findAllPostsByCursor(cursor, size + 1);
// hasNext 확인을 위해 size + 1개를 조회했으므로, 실제 응답에는 size개만 포함
boolean hasNext = posts.size() > size;
if (hasNext) {
posts = posts.subList(0, size);
}
// 다음 커서는 마지막 게시글의 ID
Long nextCursor = hasNext && !posts.isEmpty() ? posts.getLast().getId() : null;
return new CursorResponse<>(posts.stream().map(PostSummaryResponse::from).toList(), nextCursor, hasNext);
}
8. 최종 정리
기존 | 개선된 방식 |
댓글 개수를 따로 조회 | Post 테이블에서 바로 commentCounts 조회 |
findCommentCountsByPostIds() 필요 | ❌ 불필요 |
댓글 개수가 변하면 정렬이 어긋날 가능성 | ✅ Cursor 정렬 유지 가능 |
별도 쿼리 실행으로 성능 부담 | ✅ 성능 최적화 (불필요한 쿼리 제거) |
결론
- 이제 댓글 개수를 별도로 조회할 필요 없이 Post 테이블에서 바로 가져올 수 있음.
- Cursor 기반 페이지네이션에서도 댓글 개수 정렬이 유지되며, 데이터 정합성 문제가 해결됨.
- 불필요한 쿼리를 줄여 성능을 최적화함 -> 하지만 댓글 쓰기/삭제 때 추가 쿼리가 생성되기에 TRADE OF
'Project > Boilerplate' 카테고리의 다른 글
API 예외 처리 (0) | 2025.03.17 |
---|---|
@PreAuthorize를 활용한 댓글 수정/삭제 권한 관리 및 코드 개선 (0) | 2025.03.10 |
게시글 Pagination 적용 (2 / 2 - 인피니티 스크롤형 게시판) (0) | 2025.03.10 |
게시글 Pagination 적용 (1 / 2 - 리스트형 게시판) (0) | 2025.03.10 |
Mock 객체 테스트 시 필드 주의점 (0) | 2025.03.06 |