티스토리 뷰

오늘은 pagination개발을 위해 Spring에서 제공하는 Pageable & Slice interface 를 분석한 내용을 공유해보려합니다.

 

 

 

 


 

 

 

Pageable

Pageable은 org.springframework.data.domain에 포함된 interface로써 PageRequest라는 별도의 구현체가 존재합니다. 

 

PageRequest pageRequest = PageRequest.of(1,10);

Pageable pageableT0PageRequest = PageRequest.of(1,10);

Pageable pageableT0PageRequest = Pageable.ofSize(10);

 

Pageable은 PageRequest의 정적 팩토리 메서드(.of)를 사용하여 PageRequest 또는 Pageable로 생성이 가능합니다. 메서드의 '1'에 해당하는 파라미터는 'page number'를 의미하며 1번(0번부터시작)의 page의 정보를 반환하라는 의미를 가지게됩니다. 그리고 '10'에 해당하는 파리미터는 page별 데이터의 개수(size)를 의미하며 단일 페이지의 개수를 10개로 제한하라는 의미를 가지게 됩니다.

 

 

이렇게 생성된 Pageable의 정보를 통해 JPA를 실행시키기 위해서는 JpaRepository를 상속받은 interface에서 조회 메서드가 Pageable을 매개 변수로 가지도록 아래와 같이 재정의(Overloding) 해야합니다.

 

 

public interface ItemRepository extends JpaRepository<Item, Long> {

	Page<Item> findAll(Pageable pageable);

}

 

 

findAll()은 Item table에 존재하는 모든 데이터를 조회하기 위한 JpaRepository의 메서드이며, 해당 메서드에 Pageable을 매개변수로 재정의 하여 사용할 경우 Pageable에 입력된 정보를 바탕으로 JPA는 SQL을 작문하여 데이터베이스에 요청하게 됩니다.  

 

Pageable test 코드 실행 시 아래와 같은 SQL 발생

 

SELECT 구문에 LIMIT, OFFSET(조회를 시작할 기준점) 키워드가 추가되어 요청

 

위와 같은 테스트를 통해 기존 findAll()을 사용시 작성되는 SQL에 pagenation을 위한 LIMIT(레코드 조회 제한), OFFSET(조회를 시작할 기준점 지정) 키워드가 추가되는 것을 알 수 있었습니다.

 

이렇게 조회된 결과물을 pageable 객체만 받아 pagenation 구현할 경우 추가 작업 및 오버헤드가 발생됩니다. 이러한 부분을 대체할 수 있도록 Sping Data는 Slice 인터페이스에 다양한 메서드를 만들어 두었습니다. 

 

 

Slice

Slice는 Pageable과 같이 org.springframework.data.domain에 포함된 Interface로 대표적인 구현체로는 GeoPagePageImplSliceImpl가 있습니다.

 

Pageable을 매개변수로 재정의된 JpaRepository의 조회 메서드들은 아래와 같이 요청한 데이터를 조회하는 SQL이외 추가 SQL을 만들어 요청합니다.

 

위와 같은 count()함수를 사용한 SQL을 추가로 데이터베이스에 요청하여 데이터를 조회하며, 이러한 요청을 통해 만들어진 인스턴스는 Slice 인터페이스 메서드들의 작업에 기반이 되는 데이터로 사용됩니다.

 

Slice를 조금 더 이해하기 위해 Slice.java 내부에 주석으로 작성된 Slice에 대한 설명을 참고해 보도록 하겠습니다.

"A slice of data that indicates whether there's a next or previous slice available. Allows to obtain a Pageable to request a previous or next Slice. - 사용 가능한 다음 또는 이전 조각이 있는지 여부를 나타내는 데이터 조각입니다. 이전 또는 다음 조각을 요청하기 위해 Pageable을 얻을 수 있습니다."

 

저는 위와 같은 설명 내용을 통해 Pageable은 '요청한 정보를 통해 반환받은 인스턴스의 저장소'이며,  Slice는 해당 'Pageable 이전과 이후를 설명하는 정보를 가지고 있는 객체'라고 정의내릴 수 있었습니다.

 

이러한 정보를 통해 도출한 Slice 생성 절차는 아래와 같습니다.

 

1. PageRequest.of(page,size)를 Pageable에 담아 findAll()의 매개변수로 넘긴다.
2. SpingDataJPA는 Pageable에 입력된 정보를 바탕으로 pagination SQL문과 추가 정보를 위한 SQL문을 만들어 DB에 보낸다.
3. DB는 두 개의 SQL문을 통해 발생된 인스턴스들을 다시 반환한다.
4. Slice를 얻기 위해 Slice의 하위 인터페이스인 Page의 구현체(PageImpl (List<T>))에 반환된 인스턴스를 담는다.

 

위와 같은 절차를 통해 생성된 Slice를 다루기 위한 메서드는 여러가지가 있으며 이번 포스팅에서는 제가 사용한 메서드에 한하여 공유해 드리도록 하겠습니다. (더 다양한 메서드는 공식문서를 참고 부탁드립니다.)

 

 

Slice & Page Method Test

//== Page 인터페이스의 메서드 ==//

int getTotalPages();
// page의 전체 개수를 반환(페이지 번호 x)

long getTotalElements();
// 조회한 테이블의 레코드(행) 총 개수를 반환



//== Slice 인터페이스의 메서드 ==//

int getNumber();
// 현재 Slice의 번호 즉, 페이지의 번호를 반환

int getSize();
// 현재 Slice의 Size 즉, 레코드 개수를 반환

Sort getSort();
// Slice를 생성하기 위해 사용했던 sorting 매개값

Pageable getPageable()
// return PageRequest.of(getNumber(), getSize(), getSort());
// 현재 Slice를 요청하는데 사용했던 Pageble을 반환

int getNumberOfElements();
// 현재 Slice의 번호에 해당하는 레코드의 개수를 반환

boolean hasContent();
// 현재 Slice에 레코드가 존재하는지 여부를 반환

boolean hasNext();
// 현재 Slice의 다음 Slice가 존재하는지 여부를 반환

boolean hasPrevious();
// 현재 Slice의 이전 Slice가 존재하는지 여부를 반환

boolean isFirst();
// 현재 Slice가 첫 번째 Slice인지 확인 후 결과를 반환

boolean isLast();
// 현재 Slice가 마지막 Slice인지 확인 후 결과를 반환

Pageable nextPageable();
/* 현재 Slice의 다음 Slice를 
   요청하기 위한 Pageable을 반환 */

Pageable previousPageable();
/* 현재 Slice의 이전 Slice를 
   요청하기 위한 Pageable을 반환 */

Pageable nextOrLastPageable()
/* 다음 Slice에 해당하는 Pageble이 있을 경우 반환
   없으면 현재 Slice에 해당하는 Pageble 반환 */

Pageable previousOrFirstPageable()
// return hasPrevious() ? previousPageable() : getPageable();

 

Page에서는 Pageable에 대한 간단한 메서드를 제공하고 있으며, Slice에서는 조금 더 구체적인 작업을 위한 메서드를 제공하고 있습니다. 위 메서드 분석 작업을 통해 Pageable에 의해 발생된 Slice를 어떻게 조작하느냐에 따라 실제 클라이언트에 반환되는 정보와 실제 데이터베이스에 있는 정보가 다를 수도 있겠다는 생각이 들었습니다. 

 

실제 어떠한 부분이 문제가 될 요지가 있는지를 찾고자 다음과 같은 테스트를 실시해보았습니다.

 

Slice & Page 테스트 코드

 

 

테스트는 데이터베이스의 Item 테이블에 25개의 레코드를 저장 했으며,
Item 테이블의 레코드를 조회하기 위해 내림차순(DESC)으로 레코드를 10개씩 나누는 조건으로
PageRequest를 생성해서 진행했습니다. 

 

 

'PageRequest.of(0,10)' 으로 요청시 각 메서드 별 반환 값

 

'PageRequest.of(1,10)' 으로 요청시 각 메서드 별 반환 값

 

'PageRequest.of(2,10)' 으로 요청시 각 메서드 별 반환 값

 

'PageRequest.of(3,10)' 으로 요청시 각 메서드 별 반환 값( page 3은 존재하지않아야함)

 

 

마지막 반환 값 이미지에서 page에 해당하는 파라미터를 3으로 잘못된 번호를 주었음에도 불구하고 Slice의 number가 생성되었습니다. 또한 isLast가 true로 반환됨으로 이러한 사항들이 차후 비즈니스 로직 개발시 문제의 요인이 될 수도 있겠다는 판단이 들었습니다.

 

 


저는 이러한 분석을 통해 서비스 코드에 조금 더 확실한 검증 로직을 추가할 수 있게 되었으며, 기본으로 제공되는 패키지 또한 세심한 검증 과정이 필요하다는 것을 깨닫게 되었습니다.

 

 

 

 


 

 

 

마무리

오늘은 효율적인 pagination 개발을 위해 Spring에서 제공하는 Pageable과 Slice에 대해 분석해보았습니다. 해당 분석을 통해 간단한 API도 개발했으나 한번에 한 포스팅에 담기에는 내용이 과도할 것으로 판단되어 미루게 되었습니다. 다음 포스팅에서는 pageable과 Slice를 사용한 API를 간단하게라도 꼭 포스팅하도록 하겠습니다. 

 

 

 

참고자료

Spring.io 공식문서
- Pageable
- Slice
- Page

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

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday