다른 설명이 필요할까요? 이게 바로 페이지네이션입니다.
모양은 다르지만 무한스크롤 역시 페이지네이션입니다 .
전체 데이터에 중 그 일부만 전달하는 걸 페이지네이션이라고 합니다.
페이지네이션을 구현하는 방식으로는 offset방식과 cursor방식이 있습니다.
offset방식에서 클라이언트는 페이지 당 필요한 자료의 개수
그리고 현재 페이지 번호
를 전송합니다. 서버는 두 데이터를 이용해 오프셋
을 구합니다.
만약 사이트의 한 페이지에 출력되는 데이터가 10개, 사용자가 볼 페이지가 3번 페이지라면
SELECT FROM table LIMIT 10 OFFSET (3-1) 10 // 21번부터 30번까지 데이터를 출력
cursor방식은 클라이언트로부터 마지막 데이터의 고유값을 전달받습니다.
만약 사이트의 한 페이지에 출력되는 데이터가 10개, 사용자가 볼 페이지가 3번 페이지라면
SELECT * FROM table WHERE id > 20 LIMIT 10 // 21번부터 30번까지 데이터를 출력
무한스크롤을 구현할 때는 주로 cursor방식을 이용합니다.
offset방식은 데이터의 개수가 변경될 수 있기 때문에 매번 데이터를 확인하여
해당 offset수 만큼 지난간 후 데이터를 반환합니다. 모든 값을 우선 가져와서 임시 테이블에 저장한 다음 필요한 만큼만 반환하고 나머지는 버리기 때문에 속도가 느립니다.
cusror방식은 기준점이 되는 값까지 뛰어넘고 필요한 부분만 가져오기 때문에 offset보다 훨씬 빠릅니다.
offset방식은 이전에 받은 데이터를 고려하지 않고 매번 새롭게 계산하기
때문에 데이터의 잦은 추가와 삭제가 있을 경우 데이터 중복과 누락이 발생할 수 있습니다.
클라이언트와 주고받는 값이 고유하지 않다면 cursor방식 또한 중복과 누락이 발생할 수 있습니다.
Spring Data JPA는 (offset방식의) 페이지네이션 처리를 돕는 인터페이스 및 구현체를 제공하고 있습니다.
Spring Data JPA에는 클라이언트에서 전달한 매개변수를 담기 위한 Pageable, 결과 데이터를 반환하기 위한 Slice와 Page가 있습니다. (사실 Page는 Slice를 extends하기 때문에 Slice를 반환받는다고 해도 됩니다.)
Slice와 Page의 구현체 안에는 Pageable에서 요청한 만큼의 List데이터, 그리고 각각의 페이지네이션을 위해 필요한 세부 정보들을 가지고 있습니다. Slice<T>
, Page<T>
형식으로 값을 받으면 됩니다.
Pageable은 페이지네이션과 관련된 정보들을 담기 위한 인터페이스입니다.
Pageable인터페이스를 구현한 PageRequest는 페이지 당 필요한 자료의 개수
, 현재 페이지 번호
, 정렬 방식
에 대한 정보를 담고 있습니다.
다음 페이지의 존재 유무만 가지고 있을 뿐 전체 페이지수를 가지지 않습니다. 무한스크롤을 구현할 때 활용할 수 있습니다. (그렇다고 Cursor방식인 것은 아닙니다. Slice를 반환받는 쿼리는 limit과 offset을 활용합니다.)
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;
}
}
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);
}
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);
}
}
@PageableDefault
를 선언했습니다.