Skip to content

No OffSet을 사용한 페이징 처리

Jay_ edited this page Apr 12, 2022 · 3 revisions

1. 도입 이유

우리 프로젝트는 메인 기능인 모아보기, 디모, 마이페이지 등의 대부분의 조회페이지에서 사용자의 사용성을 위해서 무한 스크롤 페이징을 사용하게 되었다.
그 중, 쿼리의 효율성을 위해서 사용한 QueryDsl 내의 페이징 옵션인 limit, Offset을 사용한 페이징처리를 진행하였다.

기존코드

@Override
    public List<ArtworkMain> findAllArtWork(Long lastArtworkId, String category, Pageable paging,int sortSign) {
        return queryFactory
                .select(Projections.constructor(ArtworkMain.class,
                        artWorks.id,
                        account.id,
                        account.nickname,
                        account.profileImg,
                        artWorks.thumbnail,
                        artWorks.view,
                        artWorkLikes.count(),
                        artWorks.category,
                        artWorks.created
                ))
                .from(artWorks)
                .join(account).on(account.id.eq(artWorks.account.id))
                .leftJoin(artWorkLikes).on(artWorkLikes.artWorks.eq(artWorks))
                .offset(paging.getOffset())
                .limit(paging.getPageSize())
                .where(isCategory(category),
                        artWorks.scope.isTrue())
                .groupBy(artWorks.id)
                .orderBy(isArtWorkSort(sortSign))
                .fetch();
    }

2. 문제 상황


초반에는 더미데이터를 많이 넣지 않아서, 페이징 처리가 원활하게 이루어지는 줄 알았으나, 더미 데이터를 DB에 수만건 이상 넣어봤을 때 성능의 이슈가 있었다.
알고보니, offset 방식의 가장 치명적인 단점이 앞에서 읽은 데이터를 다시 읽어오는 풀스캔이 일어난다는 것이다. 페이징 처리가 10개씩 이루어진다고 하면, offset방식은 1번부터 10번까지를 가져오고 그 뒤의 데이터는 다시 1부터 10까지 스캔 후, 11번부터 20번까지의 10개를 다시 가져오는 식으로 진행된다.

극단적으로, 1억개의 DB데이터를 조회해야한다고 할 때 마지막 10개의 페이징 처리는 그 전 필요없는 99,999,990번의 데이터 스캔 후 그 데이터를 버린 후 10개를 가져오는 것이다.
이렇게 될 때, 당연히 페이징이 진행될 수록 점점 로딩 속도가 느려질 수 밖에 없고, 이를 개선하기 위한 다른 옵션을 알아보게 되었다. 그 중 가장 적합하다고 판단한 방법이 No Offset 방식이었다.

3. No Offset을 도입한 페이징 처리

No Offset의 경우, limit로 페이징할 페이지 개수를 지정하고, 조회 시작 부분을 인덱스로 빠르게 찾아서 매번 첫 페이지만 읽어오는 방식을 차용하고 있다.
페이징 처리를 진행하는 Where절의 id < 마지막 조회 id와 같이 직전 조회 결과의 마지막 id를 입력으로 받아서 매번 이전 페이지 전체를 건너 뛸 수 있고, 아무리 페이지가 뒤로 가더라도, 처음 페이지를 읽은 것과 같은 동일한 성능을 가지게 된다.

개선 코드

@Override
    public List<ArtworkMain> findAllArtWork(Long lastArtworkId, String category) {
        return queryFactory
                .select(Projections.constructor(ArtworkMain.class,
                        artWorks.id,
                        account.id,
                        account.nickname,
                        account.profileImg,
                        artWorks.thumbnail,
                        artWorks.view,
                        artWorkLikes.count(),
                        artWorks.category,
                        artWorks.created
                ))
                .from(artWorks)
                .join(account).on(account.id.eq(artWorks.account.id))
                .leftJoin(artWorkLikes).on(artWorkLikes.artWorks.eq(artWorks))
                .limit(10)
                .where(isLastArtworkId(lastArtworkId),
                        isCategory(category),
                        artWorks.scope.isTrue())
                .groupBy(artWorks.id)
                .orderBy(artWorks.created.desc())
                .fetch();
    }
Clone this wiki locally