본문 바로가기

IT/JPA

Spring Data JPA Pageable Paging 하기

오늘은 게시판에 필수인 Pagination 페이징처리를 Spring Data Jpa에서 어떻게 하는지와 관련 클래스를 알아보고 간단하게 만들어도 볼까합니다.

Pagination

  • 데이터베이스에서 데이터를 가져올 때 한번에 전부 가져오는 것이 아니라 정해진 Size만큼 페이징해서 가져오면서 서버의 부화를 줄여주는 기술
  • 다른 언어들의 일반적인 페이징로직에 비해 Spring Data JPA를 쓰면 동적 페이징 쿼리가 쉽습니다.

순수 JPA에서의 Pagination

먼저 간략히 순수 JPA에서의 Pagination에 대해 알아보시죠

데이터베이스에 따라 Pagination처리를 하는 방법은 다양합니다. Mysql은 좀 간단한 편이고, Oracle은 좀 복잡합니다.

JPA는 이런 여러 DB 방언(dialect)을 추상화하여 하나의 방법으로 페이지네이션을 구현할 수 있도록 제공합니다.

솔직히 스프링 사용하면서 가장 좋았던 부분중에 하나입니다.

MySQL JPA
offset setFirstResult()
limit setMaxResults()

이렇게 JPA가 추상화를 해준 덕분에 DB에 상관없이 Pagination을 손쉽게 구현가능 합니다.


그럼 오늘의 주제인 Spring Data JPA에서의 Pagination에 대해 알아 보겠습니다.

JPA에서도 Pagination에 선 계산되어야 하는 불편함등이 있는데 이런 불편함을 Spring Data JPA는 페이지 크기와 페이지 순서만 전달하면, 데이터베이스에서 해당 페이지에 해당하는 데이터만 가져올 수 있습니다.

PageRequest 생성

아래와 같이 정적 팩토리 메소드를 사용하여 PageRequest 를 생성할 수 있다.

PageRequest page = PageRequest.of(0, 10);

이때 첫번째 파라미터는 Page, Size를 의미합니다. 페이지 순서는 0부터 시작한다고 말씀드렸죠.

Pageable

// Slice.java (interface)
default Pageable getPageable() {
        return PageRequest.of(getNumber(), getSize(), getSort());
    }

//PageRequest.java
public static PageRequest of(int page, int size, Sort sort) {
        return new PageRequest(page, size, sort);
    }
  • Pageable은 인터페이스이므로 실제로 사용할 때에는 구현체 PageRequest 객체를 사용합니다.
  • 위에 보시듯 파라미터로 Page, size, sort정보를 사용합니다.
  • Page Index는 0부터 시작합니다.
  • RepositoryPageable을 전달하면, 반환 타입으로 Slice, Page를 받습니다.
    Pagination을 통한 조회 결과를 저장하는 역활을 하고 PageSlice를 상속 받습니다.

Slice

  • 전체 페이지 개수를 필요하지 않는 경우에 사용하시면 됩니다.
  • 즉 count 쿼리가 발생하지 않으니 성능 낭비가 없습니다.
  • 모바일 Infinity Scrolle을 위한 페이징에 사용하기 좋습니다.

Slice가 다음 Slice의 존재 유무를 판단하는 방법

    boolean isFirst();

    /**
     * Returns whether the current {@link Slice} is the last one.
     *
     * @return
     */
    boolean isLast();

    /**
     * Returns if there is a next {@link Slice}.
     *
     * @return if there is a next {@link Slice}.
     */
    boolean hasNext();

    /**
     * Returns if there is a previous {@link Slice}.
     *
     * @return if there is a previous {@link Slice}.
     */
    boolean hasPrevious();
//AdminBookmarkRepository.kt

PageRequest pageRequest = PageRequest.of(0, 10);
val bookMarkPage: Slice<BookMark> = AdminBookmarkRepository.findSliceBybookmark(pageRequest);

PageRequest.of(0, 10)Pageable을 전달하면, Spring Data JPA는 전달된 페이지 사이즈에 1을 더한 값으로 쿼리를 실행합니다. 즉, 총 11개의 데이터를 쿼리를 날립니다.

분명 페이지 사이즈로 10을 전달했는데, 실제 실행된 쿼리를 보면 limit 11 가 되어 있을 것입니다.

위 쿼리를 통해 불러온 데이터가 실제로 11개라면, 다음 슬라이스가 있다고 판단합니다.

Page

  • count쿼리가 날라갑니다.
  • 전체 데이터 개수 / 단일 페이지의 크기 로 계산할 수 있습니다.
  • PC 게시판 Api를 구현할 때 적합합니다.

PagingAndSortingRepository

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

    /**
     * Returns all entities sorted by the given options.
     *
     * @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
     *          {@literal null}.
     * @return all entities sorted by the given options
     */
    Iterable<T> findAll(Sort sort);

    /**
     * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
     *
     * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
     *          {@literal null}.
     * @return a page of entities
     */
    Page<T> findAll(Pageable pageable);
}

여기에 보면 findAll 메서드의 반환타입을 보시죠

  • org.springframework.data.domain.Pageable
    • 페이징을 제공하는 인터페이스입니다.
  • org.springframework.data.domain.Page
    • 페이징의 findAll() 의 기본적인 반환 메서드로 여러 반환 타입 중 하나입니다.

JpaRepository<> 를 사용할 때, findAll() 메서드를 Pageable 인터페이스로 파라미터를 넘기면 페이징을 사용할 수 있게되는 것입니다.

그럼 사용예제로 한번 보겠습니다.

//AdminBookmarkController.kt
@RestController
@RequestMapping("/admin/{adminId}/bookmarks")
class AdminBookmarkController(
    private val adminBookmarkService: AdminBookmarkService
) {
    @GetMapping
    fun getBookmarkAll(
        @RequestParam(required = false) query: String?,
        @PathVariable adminId: Long,
        @RequestParam(defaultValue = "0") page: Int,
        @RequestParam(defaultValue = "10") size: Int
    ): Page<AdminBookmarkRes> = adminBookmarkService.getBookmarkAll(
        query,
        PageRequest.of(page, size)
    ).map(BookmarkTranslator::toAdminBookmarkRes)
}

위에 DI된 AdminBookmarkService.getBookmarkAll 메서드를 보시면 PageRequest파라미터를 전달받고 있는 것을 보실 수 있습니다.

PageRequest 메서드 정의로 가서 확인 해 보겠습니다.

//PageRequest.java
public class PageRequest extends AbstractPageRequest {
}

//AbstractPageRequest.java
public abstract class AbstractPageRequest implements Pageable, Serializable {

}

PageRequestAbstractPageRequest Interface를 상속받는 것을 보실 수 있습니다.

Creates a new AbstractPageRequest. Pages are zero indexed, thus providing 0 for page will return the first page.
Params:
page – must not be less than zero.

size – must not be less than one.

파라미터로 page, size 정보를 받습니다. 위에 예에서 보시면 PageRequest.of(page, size) 이렇습니다.

// AdminBookmarkService.kt
@Service
class AdminBookmarkService(
    private val adminBookmarkRepository: AdminBookmarkRepository
) {

    fun getBookmarkAll(
        query: String?,
        pageable: Pageable
    ): Page<AdminBookmark> = if (query != null) {
            adminBookmarkRepository.SearchBookmark(
                query,
                pageable
            )
        }
}
// AdminBookmarkRepository.kt
interface AdminBookmarkRepository: JpaRepository<AdminBookmark, Long> {
    @Query("""
        SELECT *     
          FROM bookmark
        WHERE shop_id = :query OR product_id = :query
        OR customer_id = :query OR story_id = :query
        OR portfolio_id = :query
         ORDER BY id DESC
    """,
        nativeQuery = true)
    fun SearchBookmark(
        query: String,
        pageable: Pageable
    ): Page<AdminBookmark>
}

PostMan으로 Api호출해서 쿼리문 확인

  1. PageRequest.of 파라미터를 보내지 않은 경우 limit부분을 보면 page 값이 없는 것을 확인 하실 수 있습니다.

Parameter값을 확인 할 수 있게 환경설정을 해야 하는데 안해서 ?로만 보입니다.

Hibernate: 
    SELECT
        *                
    FROM
        bookmark          
    ORDER BY
        id DESC      limit ?
Hibernate: 
    select
        count(*) 
    FROM
        bookmark     

  1. PageRequest.of 파라미터를 보낸 경우 limit부분을 보면 page 값을 확인 하실 수 있습니다.
Hibernate: 
    SELECT
        *                
    FROM
        bookmark          
    ORDER BY
        id DESC      limit ?,
        ?
Hibernate: 
    select
        count(*) 
    FROM
        bookmark     

정리

오늘은 프로젝트를 할때 꼭 해야하는 Paging처리 그 중에 JPA가 빛나는 이유에 대해 알아 봤습니다.

참고로 저는 자바개발자가 아니여서 다른 언어에서의 데이터베이스의 페이징처리는 귀찮았었습니다. 근데 스프링에 Paging처리는 그저 감사할 따름이었습니다.