Jpa를 쓰다 보면 N+1문제를 신경 쓰지 않을 수 없습니다. 흔히 N+1 문제는 조회에서 많이 발생하는데요. 일반적인 N+1 문제는 FetchType.EAGER를 사용했을 때의 문제를 말합니다. 하지만 이번에 소개드릴 문제는 조회 로직에서 잘못된 반복문 사용으로 인한 문제 상황을 말씀드리겠습니다.
ManyToOne의 흔한 예시인 게시물과 댓글입니다. 게시물을 조회할 때 댓글도 함꼐 조회해서 반환해야 하는게 일반적이죠.
1번 코드를 보면
public PostResultDto getPostList(Pageable pageable){
//page 정보를 담은 pageRequest, 생성시간의 내림차순 정렬
PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(Sort.Direction.DESC, "createTime"));
//pageRequest를 이용해 모든 post를 조회 (쿼리 1개)
Page<Post> findPostObject = postRepository.findAll(pageRequest);
List<Post> postList = findPostObject.getContent();
List<PostDto>postDtoList= new ArrayList<>();
//각 post당 댓글들을 순차적으로 조회하여 저장함 (쿼리 N개)
for(Post post : postList){
List<PostComment> commentList = postCommentRepository.findPostCommentsByPostId(post.getId());
if(commentList != null){
post = post.toBuilder().commentList(commentList).build();
}
PostDto postDto = PostDto.entityToDto(post);
postDtoList.add(postDto);
}
return new PostResultDto(postDtoList,findPostObject.isLast());
}
쿼리 1개로 모든 게시물을 조회하는 것은 좋았습니다. 하지만 댓글을 조회할 때는 각 게시물마다 한 번씩 쿼리를 실행하기 때문에 총 N번의 쿼리가 생성되어 N+1 문제가 발생했습니다. 이는 초보자들이 충분히 겪을 수 있는 문제입니다. 그렇다면 어떻게 해결해야 할까요?
해결 방법은 댓글도 한 번의 쿼리로 가져오는 것입니다
public PostResultDto getPostList(Pageable pageable){
PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(Sort.Direction.DESC, "createTime"));
//게시물 정보 가져옴 (쿼리 1개)
Page<Post> findPostObject = postRepository.findAll(pageRequest);
List<Post> postList = findPostObject.getContent();
// 각 게시물의 id를 List로 저장
List<Long> postIds = postList.stream().map(Post::getId).collect(Collectors.toList());
// 게시물 id들을 이용해 모든 댓글을 한번에 가져옴 (쿼리 1개)
List<PostComment> allComments = postCommentRepository.findAllComments(postIds);
// 각 게시물 id들을 그룹핑 기준으로 하여 key : postId, value : commentList인 Map을 생성합니다.
Map<Long, List<PostComment>> commentsByPostId = allComments.stream()
.collect(Collectors.groupingBy(comment -> comment.getPost().getId()));
//이제 각 post 마다 commentList를 매핑시키고 Dto로 변환합니다.
List<PostDto> postDtoList = postList.stream().map(post -> {
List<PostComment> commentList = commentsByPostId.get(post.getId());
Post updatedPost = post.toBuilder().commentList(commentList).build();
return PostDto.entityToDto(updatedPost);
}).collect(Collectors.toList());
return new PostResultDto(postDtoList,findPostObject.isLast());
}
하나씩 따라서 작성해보시면 이해가 빠르실 겁니다.
findAllComments 메소드 입니다.
@Repository
public interface PostCommentRepository {
@Query("select pc from PostComment pc where pc.post.id in :postIds")
List<PostComment>findAllComments(@Param("postIds") List<Long>postIds);
}
IN 키워드를 사용하여 java의 for : each와 같은 효과를 가진 JPQL을 생성할 수 있습니다.
이렇게 총 두 개의 쿼리만으로 게시물을 조회하는게 가능합니다.
감사합니다.