최대한 짧게 정리해보는 페이지네이션

최창효·2023년 10월 15일
0
post-thumbnail
post-custom-banner

페이지네이션이란?

다른 설명이 필요할까요? 이게 바로 페이지네이션입니다.

모양은 다르지만 무한스크롤 역시 페이지네이션입니다 .

전체 데이터에 중 그 일부만 전달하는 걸 페이지네이션이라고 합니다.

페이지네이션 구현 방식

페이지네이션을 구현하는 방식으로는 offset방식과 cursor방식이 있습니다.

offset방식

  • mysql에서 offset은 조회를 시작할 기준점을 의미합니다. offset은 0부터 시작합니다.

offset방식에서 클라이언트는 페이지 당 필요한 자료의 개수 그리고 현재 페이지 번호를 전송합니다. 서버는 두 데이터를 이용해 오프셋을 구합니다.

만약 사이트의 한 페이지에 출력되는 데이터가 10개, 사용자가 볼 페이지가 3번 페이지라면
SELECT FROM table LIMIT 10 OFFSET (3-1) 10 // 21번부터 30번까지 데이터를 출력

cursor방식

cursor방식은 클라이언트로부터 마지막 데이터의 고유값을 전달받습니다.

만약 사이트의 한 페이지에 출력되는 데이터가 10개, 사용자가 볼 페이지가 3번 페이지라면
SELECT * FROM table WHERE id > 20 LIMIT 10 // 21번부터 30번까지 데이터를 출력

무한스크롤을 구현할 때는 주로 cursor방식을 이용합니다.

offset방식 vs cursor방식

  • offset방식은 데이터의 개수가 변경될 수 있기 때문에 매번 데이터를 확인하여 해당 offset수 만큼 지난간 후 데이터를 반환합니다. 모든 값을 우선 가져와서 임시 테이블에 저장한 다음 필요한 만큼만 반환하고 나머지는 버리기 때문에 속도가 느립니다.

  • cusror방식은 기준점이 되는 값까지 뛰어넘고 필요한 부분만 가져오기 때문에 offset보다 훨씬 빠릅니다.

  • offset방식은 이전에 받은 데이터를 고려하지 않고 매번 새롭게 계산하기때문에 데이터의 잦은 추가와 삭제가 있을 경우 데이터 중복과 누락이 발생할 수 있습니다.

  • 클라이언트와 주고받는 값이 고유하지 않다면 cursor방식 또한 중복과 누락이 발생할 수 있습니다.

Spring Data JPA와 페이지네이션

Spring Data JPA는 (offset방식의) 페이지네이션 처리를 돕는 인터페이스 및 구현체를 제공하고 있습니다.

Spring Data JPA에는 클라이언트에서 전달한 매개변수를 담기 위한 Pageable, 결과 데이터를 반환하기 위한 Slice와 Page가 있습니다. (사실 Page는 Slice를 extends하기 때문에 Slice를 반환받는다고 해도 됩니다.)

Slice와 Page의 구현체 안에는 Pageable에서 요청한 만큼의 List데이터, 그리고 각각의 페이지네이션을 위해 필요한 세부 정보들을 가지고 있습니다. Slice<T>, Page<T> 형식으로 값을 받으면 됩니다.

Pageable / PageRequest

Pageable은 페이지네이션과 관련된 정보들을 담기 위한 인터페이스입니다.
Pageable인터페이스를 구현한 PageRequest는 페이지 당 필요한 자료의 개수, 현재 페이지 번호, 정렬 방식에 대한 정보를 담고 있습니다.

Slice / SliceImpl

다음 페이지의 존재 유무만 가지고 있을 뿐 전체 페이지수를 가지지 않습니다. 무한스크롤을 구현할 때 활용할 수 있습니다. (그렇다고 Cursor방식인 것은 아닙니다. Slice를 반환받는 쿼리는 limit과 offset을 활용합니다.)

Page / PageImpl

Slice와 달리 전체 페이지수를 가지고 있습니다. 이를 계산하기 위한 count쿼리가 추가로 필요합니다.

간단 예제

Spring Data JPA를 이용해 Pageable과 Page에 대해 알아보겠습니다.

엔티티

@Entity
@Getter
@NoArgsConstructor
@ToString
public class Item {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public Item(String name) {
        this.name = name;
    }
}

Repository

public interface ItemRepository extends JpaRepository<Item,Long> {
    Page<Item> findPageBy(Pageable pageable);
}

테스트코드

    @Test
    void findPageBy() {
        // given
        itemRepository.save(new Item("item1"));
        itemRepository.save(new Item("item2"));
        itemRepository.save(new Item("item3"));
        itemRepository.save(new Item("item4"));
        itemRepository.save(new Item("item5"));

        int page = 1;
        int size = 2;
        Pageable pageable = PageRequest.of(page,size);
        
        // when
        Page<Item> result = itemRepository.findPageBy(pageable);

        // then
        Assertions.assertThat(result.getContent()).extracting("name")
                        .containsExactly(
                                "item3","item4"
                        );
        Assertions.assertThat(result.getTotalPages()).isEqualTo(3);

    }
  • 테스트를 위해 5개의 데이터를 저장했습니다.
  • 한 페이지마다 2개(size=2)의 데이터를 보여주고 있으며, 현재 사용자가 요청한 페이지는 2페이지(page=1)입니다. 그러므로 우리는 item3과 item4를 반환해야 합니다.
    또한 5개의 데이터를 2개씩 보여주므로 전체 페이지 수는 3입니다.
  • Page<Item>의 getContent() 메서드를 통해 실제 데이터를 가져올 수 있습니다.
  • Page<Item>의 getTotalPages() 메서드를 통해 전체 페이지 수를 가져올 수 있습니다. 이를 알기 위해 count쿼리가 함께 실행됐습니다.

컨트롤러

@RestController
@RequiredArgsConstructor
public class ItemController {

    private final ItemRepository itemRepository;

    @GetMapping("/item")
    public Page<Item> items(@PageableDefault(size=5) Pageable pageable) {
        return itemRepository.findPageBy(pageable);
    }
}
  • page와 size를 설정하지 않은 기본 요청(http://localhost:8080/item)을 처리하기 위해 @PageableDefault를 선언했습니다.

프론트

기본 url 요청 시

page와 size를 queryString으로 전달 시

  • page=0(첫 페이지)
  • page=1(두번째 페이지)

예제 코드

References

profile
기록하고 정리하는 걸 좋아하는 개발자.
post-custom-banner

0개의 댓글