페이지를 구현한다면 두 가지 경우를 생각해볼 수 있다. 스크롤이 끝에 다다르면 추가 요청으로 정보를 불러오는 무한 스크롤과 페이지 전환으로 다음 정보를 보여주는 페이지네이션이다.

 

(1) 무한 스크롤
(2) 페이지네이션

둘 다 sql의 limit, offset을 잘 활용하면 구현할 수 있어보인다. 하지만 spring data jpa에서 편리한 인터페이스를 제공한다. query method에 Pageable을 인자로 주면 return으로 Slice, Page, List 등의 타입을 받을 수 있다.

public interface S3ImageRepository extends JpaRepository<Image, Long> {
    Slice<Image> findSliceBy(Pageable pageable);
    Page<Image> findPageBy(Pageable pageable);
}

 

Pageable은 PageRequest를 통해 편리하게 만들 수 있다. PageRequest는 Pageable을 상속받는다.

PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id"));

Slice

Slice는 (1)무한스크롤 경우에 적합하다.

select
        i1_0.id,
        i1_0.content,
        i1_0.created_at,
        i1_0.modified_at,
        i1_0.pin_image_url,
        i1_0.title,
        i1_0.user_id 
    from
        image i1_0 
    order by
        i1_0.id desc limit ?,
        ?

findSliceBy를 실행했을 때 위와 같이 limit offset이 동작하는 걸 볼 수 있다.

boolean hasNext();

slice는 전체 개수를 조회하지 않고 limit+1로 동작하고 hasNext 메서드로 다음 데이터가 있는지 없는지를 확인할 수 있다. 무한 스크롤의 경우 다음의 존재 여부만 필요하기에 적합하다.

Page

Page는 (2)페이지네이션 구현에 적합하다.

select
        i1_0.id,
        i1_0.content,
        i1_0.created_at,
        i1_0.modified_at,
        i1_0.pin_image_url,
        i1_0.title,
        i1_0.user_id 
    from
        image i1_0 
    order by
        i1_0.id desc limit ?,
        ?;

select
        count(i1_0.id) 
    from
        image i1_0;

findPageBy를 실행했을 때 Slice 처럼 limit offset 쿼리가 동작하고 추가로 count 집계 함수가 동작하는 걸 볼 수있다.

public interface Page<T> extends Slice<T> {
    ...
	/**
	 * Returns the number of total pages.
	 *
	 * @return the number of total pages
	 */
	int getTotalPages();

	/**
	 * Returns the total amount of elements.
	 *
	 * @return the total amount of elements
	 */
	long getTotalElements();
    ....
}

Page 인터페이스는 Slice를 상속받는다. Slice의 메서드에 더해 getTotalPages, getTotalElements 같은 메서드를 사용하여 전체 페이지 수와 전체 데이터 개수를 알 수 있다. 그래서 전체 페이지 수를 알아야하는 페이지네이션에 적합하다.

응답

public Page<Image> getImages(){
    ....
    return Page<Image>
}

추가로 Page<T> 또는 Slice<T>로 응답할 때 추가 설정이 없어도 추가 정보들이 더해져 넘어가는 걸 볼 수 있다.

	"pageable": {
		"sort": {
			"empty": false,
			"sorted": true,
			"unsorted": false
		},
		"offset": 20,
		"pageNumber": 1,
		"pageSize": 20,
		"paged": true,
		"unpaged": false
	},
	"last": false,
	"totalElements": 1097,
	"totalPages": 55,
	"size": 20,
	"number": 1,
	"sort": {
		"empty": false,
		"sorted": true,
		"unsorted": false
	},
	"first": false,
	"numberOfElements": 20,
	"empty": false

위의 경우 Page<T> 응답이다.

	"pageable": {
		"sort": {
			"empty": false,
			"sorted": true,
			"unsorted": false
		},
		"offset": 20,
		"pageSize": 20,
		"pageNumber": 1,
		"paged": true,
		"unpaged": false
	},
	"size": 20,
	"number": 1,
	"sort": {
		"empty": false,
		"sorted": true,
		"unsorted": false
	},
	"first": false,
	"last": false,
	"numberOfElements": 20,
	"empty": false

위의 경우 Slice<T> 응답이다.

Page 응답에는 있는 totalElements와 totalPages 프로퍼티가 없는 것을 볼 수 있다.

사용 예시

//repository
Page<Batch> findPageBy(Pageable pageable);
    //controller
    @GetMapping("/all/batch/page")
    public ResponseEntity<Page<BatchResponseDto>> getAllBatchDto(
            @RequestParam("pageNumber") String pageNumber,
            @RequestParam("pageSize") String pageSize,
            @RequestParam("descByColumn") String descByColumn
    ) {
        Page<BatchResponseDto> batchResponse = scheduleService.getBatchResponseByPage(pageNumber, pageSize, descByColumn);
        return ResponseEntity.ok().body(batchResponse);
    }
    //service
    public Page<BatchResponseDto> getBatchResponseByPage(String pageNumber, String pageSize, String descByColumn) {
        Pageable pageable = PageRequest.of(Integer.parseInt(pageNumber), Integer.parseInt(pageSize), Sort.by(Sort.Direction.DESC, descByColumn));
        Page<Batch> page = batchRepository.findPageBy(pageable);
        return page.map(b -> batchResponseMapper.toDto(b));
    }