현 상황
현재 데이터베이스에는 다음과 같은 데이터 존재
- 유저(User): 50명
- 게시글(Post): 10,000개
- 댓글(Comment): 각 게시글당 5개씩, 총 50,000개
기존 성능 문제
기존 방식으로 전체 게시글을 조회하면 총 15,791ms 소요
또한, 10,000개의 게시글 모두 불러옴
기존 쿼리 분석
기존에는 LEFT JOIN FETCH를 사용하여 게시글과 모든 댓글을 한 번에 불러오는 방식
@Query("SELECT DISTINCT p FROM Post p " +
"LEFT JOIN FETCH p.comments c " +
"LEFT JOIN FETCH c.user " +
"LEFT JOIN FETCH p.user")
List<Post> findAllWithComments();
문제점
- 모든 댓글을 불러와서 프론트에서 Comment[].length를 확인하여 개수를 표시
- 불필요한 정보(댓글 내용, 작성자 정보 등)까지 포함됨 → 데이터 전송량 증가
- 게시글 1개당 댓글이 5개이므로, 중복된 게시글 데이터가 5배 이상 조회됨 → 성능 저하
최적화 방향
- 댓글 개수는 별도의 쿼리로 조회 (필요한 데이터만 불러오기)
- 작성자 정보는 PostUserResponse로 간소화 (username, name만 포함)
- 페이징을 적용하여 데이터 전송량 최소화
1. DTO 설계
게시글 응답을 위한 DTO
게시글 정보와 댓글 개수를 포함하는 PostSummaryResponse를 정의
@Getter
@AllArgsConstructor
@Builder
public class PostSummaryResponse {
private Long id;
private String title;
private String content;
private int likes;
private PostUserResponse user;
private long commentCount;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public static PostSummaryResponse from(Post post, Long commentCount) {
return PostSummaryResponse.builder()
.id(post.getId())
.title(post.getTitle())
.content(post.getContent())
.likes(post.getLikes())
.user(PostUserResponse.from(post.getUser()))
.commentCount(commentCount)
.createdAt(post.getCreatedAt())
.modifiedAt(post.getModifiedAt())
.build();
}
}
작성자 정보 DTO
게시글 목록에서는 username과 name만 필요하므로 최소한의 정보만 포함
@Getter
@Builder
public class PostUserResponse {
private Long id;
private String username;
private String name;
public static PostUserResponse from(User user) {
return PostUserResponse.builder()
.id(user.getId())
.username(user.getUsername())
.name(user.getName())
.build();
}
}
이유:
- author 필드를 따로 추가할 수도 있지만, 작성자 클릭 시 프로필 페이지 이동을 위해 username을 함께 보냄.
- 불필요한 User 엔티티 전체 조회를 방지하여 성능 최적화.
2. 댓글 개수만 조회하는 최적화된 쿼리
기존에는 모든 댓글을 가져왔지만, 이제 COUNT()를 활용하여 개수만 가져오기
@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);
변경된 점
- 기존: LEFT JOIN FETCH를 사용하여 모든 댓글을 조회 → 데이터 과다 로딩 문제 발생
- 개선: COUNT()를 사용하여 댓글 개수만 조회 → 데이터 전송량 및 조회 시간 감소
3. 최적화된 게시글 조회 쿼리
게시글 목록 조회 시 LEFT JOIN FETCH를 최소화하여 불필요한 데이터 로딩 방지
@Query("SELECT DISTINCT p FROM Post p " +
"LEFT JOIN FETCH p.user u " +
"ORDER BY p.createdAt DESC")
Page<Post> findAllPostSummaries(Pageable pageable);
변경된 점
- p.user는 LEFT JOIN FETCH로 가져오지만, comments는 조회하지 않음
- 페이징을 적용하여 한 번에 불러오는 데이터 개수 제한 (Pageable 사용)
4. 서비스 코드 적용
@Transactional(readOnly = true)
public Page<PostSummaryResponse> getAllPosts(Pageable pageable) {
Page<Post> posts = postRepository.findAllPostSummaries(pageable);
List<Long> postIds = posts.stream().map(Post::getId).toList();
// 현재 조회 중인 게시글들에 대한 댓글 개수 조회
List<Object[]> commentCounts = commentRepository.findCommentCountsByPostIds(postIds);
Map<Long, Long> commentCountMap = commentCounts.stream()
.collect(Collectors.toMap(
obj -> (Long) obj[0], // postId
obj -> (Long) obj[1] // commentCount
));
return posts.map(post -> {
long commentCount = commentCountMap.getOrDefault(post.getId(), 0L);
return PostSummaryResponse.from(post, commentCount);
});
}
변경된 점
- 게시글을 먼저 조회한 후, 해당 postIds에 대해서만 댓글 개수 조회 → 성능 최적화
- 불필요한 데이터 로딩 없이 PostSummaryResponse로 변환하여 반환
5. 컨트롤러에서 페이징 적용
@GetMapping
public ResponseEntity<Page<PostSummaryResponse>> getAllPosts(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size
) {
return ResponseEntity.ok(
postService.getAllPosts(PageRequest.of(page, size))
);
}
변경된 점
- 기본 페이지 크기는 10개로 설정 (size=10)
- PageRequest.of(page, size)를 사용하여 Pageable 적용
- 페이징된 Page<PostSummaryResponse> 객체를 그대로 반환
최종 성능 개선 결과
기존 방식 vs 최적화 후
전체 조회 시간 | 15,791ms | 9ms |
조회 데이터 크기 | 게시글의 모든 요소 포함 | 게시글 + 간소화 유저정보 + 댓글 개수만 조회 |
페이징 적용 | ❌ (전체 데이터 로딩) | ✅ (페이지 단위로 불러옴) |
결과 화면:
다음 목표
Cursor를 이용한 Infinity Scroll 방식의 페이징을 구현할 예정
'Project > Boilerplate' 카테고리의 다른 글
Cursor 페이지네이션에서의 정렬, 필터링 (0) | 2025.03.10 |
---|---|
게시글 Pagination 적용 (2 / 2 - 인피니티 스크롤형 게시판) (0) | 2025.03.10 |
Mock 객체 테스트 시 필드 주의점 (0) | 2025.03.06 |
Spring Boot response로 header가 추가되지 않는 현상 (0) | 2025.03.06 |
User implements UserDetails 문제 (0) | 2025.03.06 |