Springboot N+1문제 서비스 로직에서의 예시

김정훈·2024년 5월 20일
0

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을 생성할 수 있습니다.
이렇게 총 두 개의 쿼리만으로 게시물을 조회하는게 가능합니다.

감사합니다.

profile
백엔드 개발자

0개의 댓글