본문 바로가기
Side Project/Socket

페이징 쿼리 최적화

by asdft 2024. 4. 6.

들어가기 전에...

 

지금 부터 설명할, 두 코드 모두 전체 데이터를 가져오는데 사용할 수 있지만, 성능 면에서 차이가 있다.

 

  첫번째 방법의 경우 query.fetch().size()를 사용하여 전체 데이터 개수를 가져온다.

이는 쿼리를 실행하여 모든 결과를 가져온 후 리스트의 크기를 반환하는 방식이다.

이렇게 하면 전체 결과를 메모리로 가져오기 때문에 가져올 데이터가 매우 큰 경우, 성능이 저하될 수 있다.

또한, fetch().size() 메서드 호출이 실제로는 모든 결과를 가져오기 때문에 성능적으로 비효율적이다.

 

  두 번째 방법에서는 count 쿼리를 사용하여 전체 레코드 수를 가져온다.

count 쿼리는 실제 데이터를 가져오지 않고 단순히 데이터 개수만을 반환하므로, 성능상의 이점이 있다.

또한, 전체 데이터를 메모리로 가져올 필요도 없어 데이터 크기가 클 경우, 성능의 저하도 피할수 있다.

 

앞으로 2번째 방법으로 페이징 조회를 할때 사용해야 겠다.

 

그럼 이제부터 더 자세히 알아보자.


1. PageImpl< >(content, pageable, count);

PageImpl의 경우

어떠한 경우에서든지, contents쿼리 1번 + count쿼리 1번, 총 2번의 쿼리가 나가게 된다.

그리고 내가 짠 아래코드의 문제점은

 

   1.  count쿼리의 size( )를 구하기 위해 DB에서 개수를 가져오는 게 아닌 전체 결과를 메모리에 올린 후 크기를 구해

        메모리에 큰 부담을 주게된다.

   2. count가 long 타입도 아닌 int 타입으로 선언한것

 

짜고 나서 보니, 이 두가지가 가장 큰 문제였다.

그래서 좀 더 성능을 최적화 할 방법으로 뒤에 나올 2번의 방식으로 바꿔 주었다.


  @Transactional(readOnly = true)
  @Override
  public Page<PostDto> getPostsByHashTag(HashSet<Long> idList, Pageable pageable, OrderSpecifier<?> orderSpecifier) {

    List<PostDto> content = jpaQueryFactory
        .select(Projections.fields(PostDto.class,
            post.id.as("postId"),
            post.title,
            post.postContent,
            post.postType,
            post.postMeeting,
            post.postStatus,
            post.createdAt,
            post.user.userId,
            post.user.nickname.as("userNickname")))
        .from(post)
        .join(post.user, user)
        .where(isEmptyPostId(idList))
        .orderBy(orderSpecifier)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

    JPAQuery<Post> query = jpaQueryFactory
        .select(post)
        .from(post)
        .join(post.user, user)
        .where(isEmptyPostId(idList));

    int count = query.fetch().size();

    return new PageImpl<>(content, pageable, count);
  }

  private BooleanExpression isEmptyPostId(HashSet<Long> idList) {
    return !idList.isEmpty() ? post.id.in(idList) : null;
  }

 

                                size=10, page=0                                                      size=10, page=1

 

DB에 post entity의 개수가 18개 있다. 

 

결과 : 4번의 쿼리 + 메모리의 부담 으로 모든 게시물을 페이징하여 조회할 수 있었다.

 

 

 

2. PageableExecutionUtils.getPage()

  • CountQuery 최적화 : count 쿼리가 생략 가능한 경우 생략해서 처리
  • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때,
    마지막 페이지 이면서  컨텐츠 사이즈가 페이지 사이즈보다 작을 때 count 쿼리가 생략된다.
  •  (offset + 컨텐츠 사이즈)를 더해서 전체 사이즈 구함 
  @Transactional(readOnly = true)
  @Override
  public Page<PostDto> getPostsByHashTag(HashSet<Long> idList, Pageable pageable,
      OrderSpecifier<?> orderSpecifier) {

    List<PostDto> content = jpaQueryFactory
        .select(Projections.fields(PostDto.class,
            post.id.as("postId"),
            post.title,
            post.postContent,
            post.postType,
            post.postMeeting,
            post.postStatus,
            post.createdAt,
            post.user.userId,
            post.user.nickname.as("userNickname")))
        .from(post)
        .join(post.user, user)
        .where(isEmptyPostId(idList))
        .orderBy(orderSpecifier)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

    JPAQuery<Long> countQuery = jpaQueryFactory
        .select(post.count())
        .from(post)
        .join(post.user, user)
        .where(isEmptyPostId(idList));

    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
  }

  private BooleanExpression isEmptyPostId(HashSet<Long> idList) {
    return !idList.isEmpty() ? post.id.in(idList) : null;
  }

}

                       size=10, page=0                                                         size=10, page=1

 

DB에 post entity의 개수가 18개 있다.

위와 같은 페이징 쿼리를 사용할 경우, page=0일 때는 1( contents 쿼리 ) + 1( count 쿼리 ) = 2번의 쿼리가 나가지만,

page=1 (마지막 페이지, 컨텐츠 사이즈 < 페이지 사이즈) 일 때는 content를 가져오기 위한 1번의 쿼리만 나가는 것을 확인할 수 있다.

 

(offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함, 더 정확히는 마지막 페이지 이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때)

 

결과 :  3번의 쿼리를 날려서 모든 게시물을 페이징하여 조회할 수 있었다.