요청마다 데이터를 모두 주는건 매우 비효율적인 방식입니다.
예를들어 게시물이 1000개 가량 존재한다고 가정해봅시다. 누군가가 게시물 목록 조회 API를 호출할 때 마다 1000개 전부를 주면 엄청엄청 오래걸리고 부하도 심하겠죠?
따라서, 대부분의 웹은 사진과 같은 페이지로 구분해서 데이터를 전달합니다.
이렇게 데이터를 정렬하고, 페이지 크기에 따라 나눠서 전달하는 것이 바로 페이지네이션(pagination)이라고 합니다.
💡 Pagination (페이지네이션)
데이터를 정렬기준, 페이지 크기, 몇번째 페이지인지를 토대로 정보를 전달해주는 것
스프링에서 페이지네이션은 사용자가 직접 구현해서 사용할 수 있고, Spring에서 제공하는 Pageable을 사용할 수도 있습니다.
Pageable은 Spring에서 제공하는 Pagination을 위한 인터페이스 입니다.
요런 구조로 실제 구현체인 PageRequest
를 사용합니다.
PageRequest
의 생성자는 다음과 같습니다.
page
, size
, sort
를 사용하는 것을 확인할 수 있습니다.
💡 변수 설명
size
: 한 페이지당 담길 데이터의 양 ex) 10, 5, ...
page
: size를 기준으로 몇번째 페이지인지? ex) 0, 1, ...
sort
: 무엇을 기준으로 정렬할 것인지? ex) createdAt,DESC, description이것을 기준으로 요청 URL에 변수를 넘기면, 페이지네이션이 동작합니다.
ex) GET /chats/:id?member=1&page=0&size=10&sort=description,DESC
(없으면 기본값으로 동작합니다. 이 부분은 아래서 자세하게 설명할께요)
추가적으로 size
와 page
는 추상클래스인 AbstractPageRequest
에 있습니다. 페이지 넘버와 페이지 사이즈 오류처리를 확인할 수 있습니다.
Pageable
이 편리한 점은 Spring Data JPA를 사용시 직접 넘겨주면 편리하게 페이지네이션을 할 수 있다는 점 입니다.
@GetMapping("/chats/{id}")
public ResponseEntity<BaseResponse> findMessagesByChatRoomId(@PathVariable("id") Long chatRoomId,
@RequestParam(value = "member", required = true) Long memberId,
Pageable pageable) {
List<MessagesResponseDto> messages = chatService.findMessages(chatRoomId, memberId, pageable);
return new ResponseEntity<>(new BaseResponse(messages), HttpStatus.OK);
}
먼저 컨트롤러에서 인자로 Pageable
을 넣어줍니다.
public List<MessagesResponseDto> findMessages(Long chatRoomId, Long requestUserId, Pageable pageable) throws BaseException {
// 로직...
List<Message> messages = messageRepository.findAllByChatRoom_IdOrderByCreatedAt(chatRoomId, pageable);
return messages.stream()
.map(message -> new MessagesResponseDto(message, requestUserId))
.collect(Collectors.toList());
}
서비스에서 비즈니스 로직을 수행하며, Spring Data Repository에 Pageable
을 인자로 함께 넘겨줍니다.
public interface MessageRepository extends JpaRepository<Message, Long> {
List<Message> findAllByChatRoom_IdOrderByCreatedAt(Long chatRoomId, Pageable pageable);
}
단순하게 Pageable
인자 하나만 추가해도 동작할까요?
테스트 데이터 목록입니다. 총 8개의 채팅내역이 존재합니다.
public ResponseEntity<BaseResponse> findMessagesByChatRoomId(@PathVariable("id") Long chatRoomId,
@RequestParam(value = "member", required = true) Long memberId,
Pageable pageable) {
log.info("size = {}, page = {}, sorted = {}", pageable.getPageSize(), pageable.getPageNumber(), pageable.getSort());
List<MessagesResponseDto> messages = chatService.findMessages(chatRoomId, memberId, pageable);
return new ResponseEntity<>(new BaseResponse(messages), HttpStatus.OK);
}
Pageable
에 어떤 값이 들어오는지 확인하기 위해 로그를 추가해보겠습니다.
페이지 당 데이터 갯수를 2개로 설정한 페이지네이션에서 0번째 페이지를 가져와보겠습니다.
제일 처음 데이터 2개가 넘어오는걸 확인할 수 있죠?
로그에서도 정상적으로 Pageable
에 값이 넘어왔다고 확인할 수 있네요.
페이지를 1로 바꾸면 그 다음 2개의 데이터가 넘어옵니다.
저희가 sort
는 다루지 않았는데, 제 코드에서 문제점을 발견해 아래에서 자세하게 다루겠습니다.
제 JpaRepository 코드를 다시한번 보겠습니다.
List<Message> findAllByChatRoom_IdOrderByCreatedAt(Long chatRoomId, Pageable pageable);
여기서 저는 OrderByCreatedAt
과 Pageable
의 sort
를 동시에 쓰게 됩니다. 그럼 어떻게 될까요?
테스트 코드를 위와 같이 변경하고 description을 기준으로 내림차순으로 정렬해 보겠습니다.
Pageable
이 문제없이 초기화 됐습니다.
하지만, description(message)
기준으로 내림차순 정렬되지 않았습니다.
원래라면 한글인 "김"과 "하입보이"가 가장 먼저 와야하는데 말이죠.
여기서 저희는 Pageable
에 있는 정렬보다 JPA Repository의 메서드 네임 쿼리가 우선권을 가진다는 것을 유추할 수 있습니다.
Hibernate:
select
message0_.id as id1_5_,
message0_.chat_room_id as chat_roo5_5_,
message0_.created_at as created_2_5_,
message0_.description as descript3_5_,
message0_.member_id as member_i6_5_,
message0_.updated_at as updated_4_5_
from
message message0_
left outer join
chat_room chatroom1_
on message0_.chat_room_id=chatroom1_.id
where
chatroom1_.id=?
order by
message0_.created_at asc, // 정렬기준 1
message0_.description desc limit ? // 정렬기준 2
실제 쿼리를 확인해보면 createdAt
으로 먼저 정렬한 뒤에, description
으로 정렬하는 것을 확인하실 수 있습니다.
SQL에 익숙하신 분은 아시겠지만, createdAt
으로 정렬하고 createdAt
이 같다면, description
으로 내림차순 정렬을 하는 쿼리 입니다.
💡 Pageable보다 JPA Repository의 메서드 네임 쿼리가 우선권을 가진다.
여기서 createdAt
을 지우고 다시 요청해보겠습니다.
public interface MessageRepository extends JpaRepository<Message, Long> {
List<Message> findAllByChatRoom_Id(Long chatRoomId, Pageable pageable);
}
정상적으로 message
를 기준으로 내림차순 정렬되는 것을 확인하실 수 있습니다.
💡 따라서
Pageable
을 사용할 때는, 의도한 바가 아니라면 메서드 네임드 쿼리에 정렬 기준을 잡지 않는 것이 좋을 것 같습니다.
만약 Pageable
을 쓰고 아무 인자도 주지 않으면 어떻게 될까요?
GET /chats/:id?member=1
size = 20, page = 0, sorted = UNSORTED
로 기본값을 확인할 수 있습니다.
이 부분은 PageableHandlerMethodArgumentResolverSupport
에서 확인할 수 있는데요, 저희가 따로 기본설정 해주지 않으면 fallbackPageable
으로 위에서 확인한 20, 0, UNSORTED의 값으로 반환합니다.
인자를 Pageable
에 넣어주는 PageableHandlerMethodArgumentResolver
클래스에서 resolveArgument
메서드를 확인해보겠습니다.
getPageable
을 통해 기본값 페이지를 얻어오거나 입력받은 인자를 사용하게 됩니다.
그렇다면, Pageable
의 기본값은 어떻게 설정할까요? 바로 @PageDefault
를 사용해 설정할 수 있습니다.
@GetMapping("/chats/{id}")
public ResponseEntity<BaseResponse> findMessagesByChatRoomId(@PathVariable("id") Long chatRoomId,
@RequestParam(value = "member", required = true) Long memberId,
@PageableDefault(page = 1, size = 2, sort = "description", direction = Sort.Direction.DESC) Pageable pageable) {
log.info("size = {}, page = {}, sorted = {}", pageable.getPageSize(), pageable.getPageNumber(), pageable.getSort());
List<MessagesResponseDto> messages = chatService.findMessages(chatRoomId, memberId, pageable);
return new ResponseEntity<>(new BaseResponse(messages), HttpStatus.OK);
}
@PageableDefault(page = 1, size = 2, sort = "description", direction = Sort.Direction.DESC) Pageable pageable)
설정 목록은 page
, size
, sort
(정렬기준), direction
(정렬방향) 입니다.
로그도 정상적으로 설정한 기본값이 출력되고,
결과도 동일합니다.
Slice<Message> findByChatRoom_Id(Long chatRoomId, Pageable pageable);
Page<Message> findByChatRoom_Id(Long chatRoomId, Pageable pageable);
List<Message> findByChatRoom_Id(Long chatRoomId, Pageable pageable);
가장 기본인 List
말고도 반환 타입을 지정할 수 있습니다. 각자의 반환타입을 살펴보겠습니다.
Slice
: 현재 페이지에서 이전과 다음이 있는지만 알 수 있습니다. 무한 스크롤 처럼 데이터양이 상당히 많고, 전체 페이지 갯수가 필요없는 기능에 사용됩니다.
Page
:Slice
를 상속받고, 총 페이지를 알 수 있습니다. 따라서 count 쿼리가 한번 더 나갑니다. 일반적인 게시판에서 사용됩니다.
Slice
는 어떻게 앞과 뒤에 페이지가 있는지 알 수 있을까요? 그 비밀은 Pageable
로 넘긴 페이지 사이즈에 있습니다.
넘긴 페이지 사이즈에 +1을 더해서 쿼리를 날리는데요, 여기서 만약 조회가 된다면 다음 페이지가 있다고 판단하게 됩니다.
계속 미루던 Pageable에 대해 공부를 마쳐서 기쁘네요. 긴 글 읽어주셔서 감사드리고 언제나 잘못된게 있다면 지적 부탁드립니다. 🙇🏻
https://wonit.tistory.com/483
https://zayson.tistory.com/entry/Spring-Data-JPA%EC%9D%98-Page%EC%99%80-Slice
https://tecoble.techcourse.co.kr/post/2021-08-15-pageable/
https://hudi.blog/spring-data-jpa-pagination/