개요
현재 진행 중인 개인 프로젝트에서 사용자가 저장한 이미지를 보여주는 개인 갤러리를 구현했다. 여기서 pagination을 적용해야 했고, Spring Data JPA로 어렵지 않게 구현할 수 있다는 것을 알게되었다. 프로젝트에 적용하면서 학습한 것을 여기에 정리하려고 한다.
Pagination 이란?
사용자가 요청했을 때 데이터베이스에 있는 수천, 수만, 수백만 줄의 데이터를 모두 한번에 조회하여 제공한다면 서버의 부하가 굉장히 클 것이다.
- 이를 방지하기 위해서 대부분의 서비스에서는 데이터를 일정 길이로 잘라 그 일부분만을 사용자에게 제공하는 방식을 사용한다.
- 사용자는 현재 보고 있는 데이터의 다음, 이전 구간 혹은 특정 구간의 데이터를 요청하고, 전달한 구간에 해당하는 데이터를 제공받는다.
이 기능을 pagination (혹은 paging) 이라고 한다. Pagination은 아주 많은 상황에서 자주 사용되는데, 대표적인 예로는 게시판 목록이 있다.
Spring Data JPA의 Pagination
DB 마다 pagination을 위해 사용되는 쿼리는 다 다르고, 난이도도 천차만별이다.
- 예를 들어, MySQL에서는 offset, limit 으로 상대적으로 간단히 처리가 가능하지만, Oracle의 경우 상당히 복잡하다.
JPA는 이런 여러 DB 별 방언(dialect)을 추상화하여 하나의 방법으로 페이지네이션을 구현할 수 있도록 제공해준다.
하지만, JPA로 페이지네이션 기능을 구현하는 작업은 생각보다 까다롭다.
- 전체 데이터 개수를 가져와서 전체 페이지를 계산해야하고, 현재 페이지가 첫번째 페이지인지, 마지막 페이지인지도 계산해야하고, 예상치 못한 페이지 범위를 요청받았을 때 예외처리도 해야한다.
- 물론, JPA 없이 DB에 직접 쿼리하는 방식보다는 훨씬 편리하지만, 여전히 신경써야 할 부분들이 많이 보인다.
Spring Data JPA는 이런 pagination도 추상화되어 있다. 각 페이지의 설정만 조정하여 전달하면 DB에서 해당 설정에 맞는 부분의 데이터만 조회할 수 있다.
Pagination 구현
Pagable & PageRequest
Spring Data JPA로 pagination을 사용하기에 앞서, Pagable 에 대해서 알아보자.
Pageable 은 Spring Data에서 제공하는 pagination 정보를 담기 위한 인터페이스이다. 페이지 번호와 단일 페이지의 개수를 담을 수 있다. 이 Pagable을 구현하기 위해 PageRequest 라는 구현체도 제공한다.
Pageable 생성
아래와 같이 PageRequest 의 static method를 이용하여 Pageable 을 구현할 수 있다.
Pageable pageable = PageRequest.of(PAGE, SIZE);
여기서 PAGE 는 페이지의 순서 (index) 이고, SIZE 는 현재 페이지의 크기를 뜻한다.
참고
page의 순서는 0부터 시작한다.
Pageable 정렬
아래와 같이 조회된 페이지를 어떤 방식으로 정렬할지 설정도 가능하다.
PageRequest.of(PAGE, SIZE, Sort.Direction.ASC, "id"); // id 기준 오름차순
PageRequest.of(PAGE, SIZE, Sort.Direction.DESC, "id"); // id 기준 내림차순
이렇게 구현된 Pageable 을 아래와 같이 Spring Data JPA repository에 전달하면 된다.
public interface ImageRepository extends JpaRepository<Image, Long> {
Page<Image> findAllByOwnerId(Long ownerId, Pageable pageable);
}
Page<Image> savedImages = userService.findSavedImages(loginUser.id(), pageable)
위의 결과를 DTO나 VO로 매핑도 가능하다.
Page<ImageVO> savedImages = userService.findSavedImages(loginUser.id(), pageable)
.map(image -> new ImageVO(image.getId(), image.getPrompt(), image.getUrl()));
Page & Slice
위의 코드를 다시 보면, Spring Data JPA의 페이징 반환 타입은 Page 라는 것을 확인할 수 있다. 이 Page 는 어떤 기능을 갖고 있는 것일까?
사실, Spring Data JPA 레포지토리에 Pageable 을 전달하면, 반환 타입으로 위에서 나온 Page 와 Slice 를 받을 수 있다. 두 인터페이스 모두 pagination을 통한 조회 결과를 저장하는 역할을 한다.
참고로, Page는 Slice를 상속받는다.
Slice 는 별도로 count 쿼리를 실행하지 않기 때문에, 전체 페이지의 개수와 전체 엔티티의 개수를 알 수 없지만, 불필요한 count 쿼리로 인한 성능 낭비는 발생하지 않는다. 따라서, infinite scroll과 같이 전체 페이지 개수가 굳이 필요하지 않는 상황에 자주 사용된다.
반대로 Page 는 count 쿼리를 실행하여, 전체 데이터 개수와 전체 페이지 개수를 계산할 수 있다. 따라서, 우리가 흔히 볼 수 있는 게시판의 pagination UI 등을 구현할 때 적합하다.
References
JPA Pagination, 그리고 N + 1 문제
1. Pagination 게시판 기능을 제공하는 웹 어플리케이션에 접속하여 게시물 목록을 요청하는 경우를 상상해봅시다. DB…
tecoble.techcourse.co.kr
'Spring' 카테고리의 다른 글
[Spring] Filter에서 발생한 예외 핸들링하기 (1) | 2023.12.25 |
---|---|
[Spring / S3] S3Mock을 사용하여 S3 테스트하기 (0) | 2023.12.24 |
[Spring] Spring에서 API 문서 자동화하기 (3) (0) | 2023.12.24 |
[Spring] Spring에서 API 문서 자동화하기 (2) - Spring REST Docs (0) | 2023.12.24 |
[Spring] Spring에서 API 문서 자동화하기 (1) - Swagger (0) | 2023.12.24 |