훈훈훈

Spring boot :: QueryDSL을 사용해서 No Offset Paging 구현하기 본문

Spring Framework/개념

Spring boot :: QueryDSL을 사용해서 No Offset Paging 구현하기

훈훈훈 2021. 1. 10. 03:20

어느 날  땡땡이라는 API를 호출 시, 조건에 만족하는 모든 데이터를 뿌려주는 쿼리가 실행되는 것을 발견하였다. 프론트에서 모든 데이터를 받고 페이징을 처리하는 구조였던 것이다. 이런 구조는 데이터가 많이질수록 부하가 발생할 수 있는 구조이기 때문에 서버에서 페이징을 처리하는 구조로 변경하기로 했다. 

 

페이징은 Offset과 limit을 사용하는 방식이 주로 사용되는데, 문제는 해당 API를 사용하는 웹 페이지에는 페이지 버튼이 존재하지 않았기 때문에 어떤 방법을 사용하여 페이징을 구현할까 고민을 하게 되었다.

현재 데이터를 보여주는 방식은 아래 사진처럼 화살표를 클릭하면 다음 데이터가 나오는 구조였다. 따라서 기존의 페이징 방식인 버튼(페이지 번호)을 사용하는 방식이 아닌 페이지 번호가 없는 No Offset 방식으로 개발하기로 하였다.

 

 

 

No Offset에 관한 내용은 구글링을 하다가 jojoldu님의 블로그 글을 보고 알게되었다. 

페이징 최적화 내용을 찾아보다가 기존의 Offset, Limit 방식 성능이 잘 나오는 No Offset 방식을 알게 되었고, 한번 실무에 적용해보고 싶었던 방법이었다.

 

No Offset이 기존의 Offset, Limit 방식보다 성능이 잘 나오는 이유는 참고한 블로그 글에서 자세히 설명하지만 간단히 말하자면 Offset, Limit 방식의 페이징은 페이지 번호가 증가할수록 성능이 떨어지게 된다. 그 이유는 쿼리를 살펴보면 좀 더 쉽게 이해할 수 있다.

SELECT * FROM '테이블명' WHERE '조건' ORDER BY '조건' OFFSET '페이지 번호' LIMIT '페이지 사이즈'

원하는 페이지 번호의 데이터를 가져오기 위해서는 그 이전에 데이터를 가져오고 나서 추가로 페이지 사이즈만큼 데이터를 가져오게 된다. 예를 들어 Offset = 10, limit = 10 값을 가져온다고 하면, Offset 1부터 10까지 데이터를 가져온 후 Offset 10의 Limit 값(10) 만큼 가져오게 된다. 즉, 이전 데이터까지 모두 불러오기 때문에 페이지 번호가 커질수록 성능이 낮이질 수밖에 없는 방법이다.

 

반면에 No Offset은 Offset, Limit 방법처럼 이전 데이터를 모두 가져오는 방식이 아닌 인덱스로 필요한 만큼만 찾게 되기 때문에 페이지 크기와 관계없이 빠르게 조회할 수 있다.

SELECT * FROM '테이블명' WHERE '조건' AND 'id < 마지막 조회된ID' ORDER BY '조건' LIMIT '조건'

 

코드 구현

이제 QueryDSL로 구현하는 코드를 살펴보자.

필자는 Spring Boot와 Kotlin으로 어플리케이션을 개발하고 있기 때문에 예제 코드는 Kotlin(코틀린)으로 작성하였다.

 

그전에 다시 한번 No Offset 쿼리 문을 살펴보면 Where 절에 다중 파라미터가 들어가게 된다. 

SELECT * FROM '테이블명' WHERE '조건' AND 'id < 마지막 조회된ID' ORDER BY '조건' LIMIT '조건'

그중 And 뒤에 있는 ID 조건 같은 경우 첫 번째 쿼리를 날릴 때는 마지막으로 조회된 ID 정보를 같이 보낼 수가 없다. (아직 조회한 데이터가 없기 때문에) 따라서 첫 번째 쿼리는 해당 조건에 null이 들어갈 수밖에 없다. 따라서 첫 번째 쿼리에는 null, 두 번째 조건부터 Id 값이 들어오기 때문에 동적 쿼리를 생성할 필요가 있다. 

 

QueryDSL 같은 경우 Where 절에 컴마(,)를 사용해서 여러 조건을 넣을 수 있고 조건 중 만약 null이 포함되어 있다면 해당 조건은 패스하기 때문에 쉽게 동적 쿼리를 만들 수 있다.

 

구현한 코드는 아래와 같다.

class AccountQuerydslRepository{

    @Autowired @Resource(name = "jpaQueryFactory")
    lateinit var query: JPAQueryFactory

    val qAccount = QAccount.account

    fun getAccountsUsingNoOffset(accountId: Long, marketingApproved: Boolean ,pageSize: Long): List<Account> {
        return query
            .select(qAccount)
            .from(qAccount)
            .where(
                gtAccountId(accountId),
                qAccount.marketingApproved.eq(marketingApproved),
            )
            .orderBy(qAccount.id.asc())
            .limit(pageSize)
            .fetch()
    }

    private fun gtAccountId(accountId: Long?): BooleanExpression? {
        if (accountId == null) {
            return null;
        }
        return qYoutubeVideo.id.gt(youtubeVideoId)
    }
}

 

QueryDSL을 사용해서 마케팅 정보 값을 조건으로 유저 정보를 가져오는 getAccountUsingNoOffset 함수와 조건에 맞는 AccountId들을 리턴해주는 gtAccountId 두 개의 함수를 만들었다.

 

gtAccountId 함수는 Accountid가 첫 번째 쿼리는 null을 값으로 들어가고 두 번째 쿼리부터 마지막으로 조회된 ID를 기준으로 페이지 사이즈만큼 AccountId를 리턴해주는 로직을 가지고 있다.

 

 

 

참고 

jojoldu.tistory.com/528

 

1. 페이징 성능 개선하기 - No Offset 사용하기

일반적인 웹 서비스에서 페이징은 아주 흔하게 사용되는 기능입니다. 그래서 웹 백엔드 개발자분들은 기본적인 구현 방법을 다들 필수로 익히시는데요. 다만, 그렇게 기초적인 페이징 구현 방

jojoldu.tistory.com

www.novatec-gmbh.de/en/blog/art-pagination-offset-vs-value-based-paging/

 

The art of pagination – Offset vs. value based paging | Novatec

For large datasets a Paginator is required. This article describes why data should be read as subsets, what Pagination is and how it affects the performance

www.novatec-gmbh.de

 

 

 

Comments