Spring/Spring Boot

[Spring] 무한스크롤 구현 및 성능 개선 하기 - No Offset 페이지네이션

민돌v 2022. 8. 12. 17:21

안녕하세요 오늘은 페이지네이션의 성능을 개선 시키는 방법 중 하나인 no-offset 페이지네이션에 대해 기록을 남겨보고자 합니다!

 


예전에 스프링 페이지네이션에 대해 포시팅한적이 있는데

이번에 실무에서 무한스크롤을 적용해야해서 직접! 사용해볼 기회가 생겨 더 자세한 내용을 기록으로 남기고자 합니다!!

 

👏🏻 이전 포스팅해서는, 아래와 같은 방법을 커서 페이지네이션이라고 칭했는데, 여기서 말하는 No Offset 과 같은 의미의 단어로 말했습니다.

jojoldu님의 글을 보고, MySql 에서 Cursor 라는 키워드를 다른 의미로 쓰고있다는걸 알게 되어, 커서페이징을 👉 No offset 이라 말했습니다.

 

jojoldu님의 블로그글을 많이 참고하였으며, 아래의 순서대로 진행됩니다 😊

  1. No offset 이란
  2.  No offset 페이지네이션 과 offset 페이지네이션의 성능 차이
  3.  QueryDsl 로 no offset 페이징 구현하기
  4. Response 에 다음페이지 Link 삽입하기

1. No offset 이란

📌 no-offset 페이지네이션이란

no offset 페이지네이션이란, 말 그대로 offset 을 사용하지 않고 페이지네이션을 진행한다는 말입니다.


sql로 예를 들자면,

  • 일반적인 페이지네이션은, limit과 offset 명령어를 이용하면, offset(어디부터) limit(몆개의) 데이터를 불러올지 결정하고
  • 이를 이용해, offset을 페이지 넘버로 활용을 합니다,
    (offset = page size * page number)
  • ex) select * from tabale_name limit 20 offset 0;


no offset 방식은 이 offset - 즉 몆번째 부터 시작할 것인지를 offset을 사용하지 않고 판단하는 것에 목적을 둡니다.

 


📌  no-offset 페이지네이션 사용하기

no offset 페이지네이션을 사용하는 방법은 생각보다 간단합니다.

offset, 즉 페이지 넘버를 대신해서, 탐색을 해줄 위치로 이동시켜줄 무엇인가만 있으면 됩니다.

 

sql에서는 우리가 흔하게 사용하는 where indxe = ? 라는 조건문을 통해 쉽게 특정 위치를 지정할 수 있습니다.

아주 간단하게 이렇게 사용할 수 있는것이죠

ex)  select * from tabale_name where idx > 0 limit 20;

 


2.  No offset 페이지네이션 과 offset 페이지네이션의 성능 차이

 

📌  no offset 페이지네이션을 사용해야하는 이유

사실 우리가 페이지네이션 기능의 성능을 개선시키고자 하는 큰 이유는, 가장 기본적인 off-set 기반 페이지네이션이 full Scan 방식을 띈다는 것입니다.

 

📌 off set 페이지네이션 성능

offset 페이지네이션의 성능은, 해당 페이지 수 까지, 쭈----욱 탐색하고 필요한 데이터를 불러오기 때문에 페이수가 늘어날수록 성능이 저하됩니다.

  1. offset 첫번째 페이지 성능
  • full scan 방식으로 탐색이 되고, 첫번째 페이지이기 때문에 20개의 데이터만 조회가 되었습니다.

  1. offset 마지막 페이지 성능
  • 단순하게 700만개의 데이터를 삽입하고, 테스트를 해보았습니다.
  • 실제 조회하는 Actual Rows를 보면, 필요없는 앞의 700만개의 데이터를 모두 조회한 후, 20개의 데이터를 조회합니다,
  • 단순 시간만 보았을 때, 조회 속도 또한 느려졌습니다.

 


📌 no-offset 페이지네이션 성능

이번에는 no-offset 페이지네이션을 사용해 보겠습니다.

  1. 첫 페이지 조회
  2. 마지막 페이지 조회

    놀랍게도, 첫페이지를 조회할 떄와, 마직막 페이지를 조회할 때의 차이가 거의 없습니다.

📌 앞의 offset 과 단순 조회 시간만 비교한다면, 60,543배의 속도차이가 발생합니다.

  • 믈론, 실제상황과 조회하는 데이터의 양 등을 고려해야겠지만
  • 확실한건! no-offset 방식은 페이지가 늘어나도, 동일한 성능을 보장한다..!! 입니다!!


하지만, 모든 상황에서 적용할 수 있는 것은 아닙니다.

 


3.  QueryDsl 로 no offset 페이징 구현하기

이제 Spring 에서 no Offset 페이지징을 구현해보겠습니다

  • (단순 예제라서 말이 맞지않을 수 있으니 로직만 보세용)

 

Controller

controller 에서 pageable을 파라미터로 받으면

각각의 요소들을 파라미터로 받아 클라이언트 친화적으로 Pageable 객체를 생성할 수 있습니다.

@GetMapping("/paging")
public ResponseEntity<Page<dto>> getList(
    @RequestParam(value = "idx", defaultValue = "0") long Idx,
    @RequestParam(value = "search", required = false) String search,
    @PageableDefault(size = 10, sort = "idx", direction = Sort.Direction.ASC) Pageable pageable) {
    return ResponseEntity.ok(pagingService.getList(pageable, searchText));
}

 

offset 을 사용하지 않고, 어떤 아이템 다음 뭐가 있는지를 확인하는 것이기 때문에, 어떤 아이템의 idx 를 받아옵니다,

  • (통상적 무한 스크롤의 마지막 아이템)

 

Repository 

@Override
public Page<dto> getNoOffsetPaging(final Pageable pageable,
    final String searchText, final long idx) {

    QueryResults<dto> results =
        queryFactory.select(new QDto(
                    qdtoidx,
                    qdto.param... // dto 담길 내용 등등
                )
            )
            .from(q테이블이름)
            .where(
                q테이블이름.likeUserProfile(searchText) //검색 조건
                    .and((q테이블.idx.gt(eventPhotoIdx)))	//gt 조건 이용
            )
            .orderBy(q테이블이름.idx.asc())
            .limit(pageable.getPageSize())
            .fetchResults();

    return new PageImpl<>(results.getResults(), pageable, results.getTotal());
}

 

Contoller Header 에 Link 삽입

이번에는 추가적인 옵션으로, 조금 더 Rest 에 다가가기위해, 응답 메세지에 next page 링크를 넣어보겠습니다.

1. HttpServletRequest 로 요청하는 클라이언트의 상태를 받아옵니다.

  • 요청 url
  • 파라미터 정보 등

2. 페이징 마지막 페이지 여부를 Spring Page 객체에서 제공해주는 isLast() 로 체크해줍니다.

3. 다음 페이지가 있으면 ResponseEntity의 Header 객체를 만들어 link 를 담은 키값에 다음 페이지의 링크를 담아줍니다!

@GetMapping("/paging")
public ResponseEntity<Page<dto>> getList(
    @RequestParam(value = "idx", defaultValue = "0") long Idx,
    @RequestParam(value = "search", required = false) String search,
    @PageableDefault(size = 10, sort = "idx", direction = Sort.Direction.ASC) Pageable pageable,
    HttpServletRequest httpServletRequest){
	
    Page<dto> responses = pagingService.getList(pageable,
        search, idx);

    HttpHeaders headers = getHeaderWithNextPage(responses, httpServletRequest);

    return ResponseEntity.ok()
        .headers(headers)
        .body(responses);
  }
  
  private HttpHeaders getHeaderWithNextPage(final Page<dto> responses, final HttpServletRequest httpServletRequest) {

        HttpHeaders headers = new HttpHeaders();

		//마지막 페이지가 아니라면 = 다음페이지가 존재한다면 header 에 Link 삽입
        if (!responses.isLast()) {
            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();

            params.add(idx", responses.getContent().get(
                responses.getNumberOfElements() - 1).getIdx().toString());
            params.add("search", httpServletRequest.getParameter("search"));

            String nextPageUri = ServletUriComponentsBuilder.fromRequestUri(httpServletRequest)
                .replaceQueryParams(params)
                .build()
                .encode()
                .toUriString();

            headers.add("Next-Page", nextPageUri);
        }

        return headers;
    }

 

제가 한 요롷게 헤더에 링크를 담는 코드는, 하나의 방법일 뿐이니 참고만 해주시고 더 좋은 방법이 있다면

피드백 해주시면 감사하겠습니다 :)


 

no offset 페이지네이션을 사용할 때

👏🏻 위의 예시와, 실제 실행계획을 보면 알 수 있듯이, where 조건절을 타고 들어가는 건, 해당 컬럼에 인덱스가 걸려있어야합니다.

그래야만 index range scan으로 조회가 되기 때문에, 유의미한 결과를 낼 수 있습니다.

 

📌따라서 다음과 같은 조건들이 붙습니다.

1. 정렬된, 인덱스 값이 있어야한다.
2. 인덱스 key 값에 중복이 있으면 안된다. (데이터가 누락될 수 있음)
3. 페이징 버튼(1,2,3) 을 사용하는 경우, 사용이 어렵다

  • 데이터의 누락을 피하기 위해서는, 반드시 연결된 인덱스를 조건으로 위치를 이동시켜야 합니다.
  • 즉, 페이징 버튼을 사용한다면 (1 - > 9) (5 -> 1) 인덱스의 값이 무조건적인 규칙에의해서, 정해진 값이라는게 보장되지 않는다면, 특정 위치를 파악하기에는 매우 힘든일입니다.

 

📌 이런 조건들로 보았을 때 No-Offset 은

정렬되고 중복이존재하지않는 인덱스값을 가지는, 무한 스크롤 방식에 유용하다고, 생각됩니다.

 

Offset 기반 페이지네이션은 우리가 원하는 데이터가 ‘몇 번째’에 있다는 데에 집중하고 있다면, 커서 기반 페이지네이션(여기서는 No Offset 이라 칭함) 은 우리가 원하는 데이터가 '어떤 데이터의 다음'에 있다는데에 집중합니다.

 

단, No-Offset 방식은, 그저 하나의 방법일 뿐, 모든 것은 요구사항과 주어진 상황에 따라 달라진다고 생각됩니다

 


참고