본문 바로가기

개발/Spring

[JPA] 게시글 1000만건 처리하기

 

페이징 + Querydsl

 

1. Entity를 직접 조회

 public Page<Post> getPostsList(Pageable pageable, String keyword) {

    BooleanBuilder builder = new BooleanBuilder();

    if(StringUtils.hasText(keyword)) {
        builder.and(post.title.like("%" + keyword + "%"));
    }

    List<Post> postsList = query
        .selectFrom(post)
        .where(builder)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .orderBy(post.id.desc())
        .fetch();

    //count 구하기
    JPAQuery<Long> countQuery = query
        .select(post.count())
        .where(builder)
        .from(post);

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

}

 

2. 커버링 인덱스 사용

public Page<Post> getPostsCoveringIndexList(Pageable pageable, String keyword) {

  BooleanBuilder builder = getBooleanBuilder(keyword);

  List<Long> ids = query
        .select(post.id)
        .from(post)
        .where(builder)
        .orderBy(post.id.desc())
        .offset(offset)
        .limit(size)
        .fetch();

  List<Post> postsList = query
        .selectFrom(post)
        .where(post.id.in(ids))
        .orderBy(post.id.desc())
        .fetch();

  //count 구하기
  JPAQuery<Long> countQuery = query
        .select(post.count())
        .from(post)
        .where(builder);


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

}

 

 

3. DTO 이용

public Page<PostListResponseDto> getPostsDtoList(Pageable pageable, String keyword) {

  BooleanBuilder builder = getBooleanBuilder(keyword);

  List<Long> ids = toPostIds(pageable.getOffset(), pageable.getPageSize(), builder);

  List<PostListResponseDto> postsList = query
        .select(new QPostListResponseDto(
              post.id,
              post.title,
              user.name,
              post.createdDate)
        )
        .from(post)
        .leftJoin(post.user, user)
        .where(post.id.in(ids))
        .orderBy(post.id.desc())
        .fetch();


  //count 구하기
  JPAQuery<Long> countQuery = query
        .select(post.count())
        .from(post)
        .where(builder);

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

 

 

정리

- 처음 커버링 인덱스를 적용했을 때 성능에 큰 차이가 있었다.

- DTO로 변환했을 때에는 미묘한 차이만 있었다. 그래도 보내는 쿼리 수가 줄어든 것에 만족

- 1억건으로 데이터를 늘렸을 때 최적화에 한계를 느꼈다.

- 초반 페이지는 속도가 빠른데 1억 건 이상 데이터가 들어가니 count query 수행 시간이 오래 걸렸다.

- 페이지가 뒤로 갈수록  offset으로 인한 속도 저하가 있었다.

 

 

 

번외) No Offset

public Slice<PostListResponseDto> paginationNoOffset(
	Long postId, Long userId, Pageable pageable, String keyword) {

  BooleanBuilder builder = new BooleanBuilder();

  if(postId != null) {
      builder.and(post.id.lt(postId));
  }

  builder.and(post.user.id.eq(userId)).and(getBooleanBuilder(keyword));

  List<PostListResponseDto> posts = query
            .select(new QPostListResponseDto(
                post.id,
                post.title,
                user.name,
                post.createdDate)
            )
            .from(post)
            .leftJoin(post.user, user)
            .where(builder)
            .limit(pageable.getPageSize()+1)
            .orderBy(post.id.desc())
            .fetch();

  return getSliceImpl(pageable, posts);
}

// 다음 페이지가 있는지 확인
private static SliceImpl<PostListResponseDto> getSliceImpl(Pageable pageable,
  List<PostListResponseDto> posts) {

  if(posts.size() == (pageable.getPageSize() + 1)) {
      return new SliceImpl<>(posts.subList(0, pageable.getPageSize()), pageable, true);
  }

  return new SliceImpl<>(posts, pageable, false);
}

 

 

- no offset 방식은 확실히 빨랐다.

- 더보기 버튼만 이용하여 다음 페이지를 중첩하여 불러왔다.

- 그러나 게시판이라면 개인적으로 페이지 이동이 꼭 필요하다고 생각했다.