Spring Pagination
Pagination in Spring Webflux and Spring Data Reactive
1. 개요
본 글에는 다음 세 가지에 대해 작성되어 있다.
- 데이터 조회에 있어서 Pagination의 중요성
- Pagination 사용에 있어서 Spring Data Reactive와 Spring Data의 차이점
- Pagination 구현 예제
2. Pagination의 중요성
Pagination은 대용량 데이터를 응답할 때 필수적이다. Pagination은 대용량 데이터를 작은 덩어리인 페이지 단위로 나누어 데이터를 효율적으로 조회하고 응답할 수 있도록 한다.
3. Spring Data와 Spring Data Reactive의 차이
Spring Data는 Java 애플리케이션의 데이터 접근을 단순화하고 향상시키는 것을 목표로 하는 Spring Framework의 프로젝트 중 하나이다. Spring Data는 보일러 플레이트 코드를 줄이고 모범 사례를 촉진하여 코드를 단순화하는 공통 추상화 및 기능을 제공한다.
Spring Data Pagination example에서 설명했듯이 page, size, sort 로 구성된 PageRequest를 사용하여 페이지를 구성하고 요청할 수 있다. Spring Data에는 pagination과 sorting 추상화를 사용하여 엔티티 검색 메서드를 제공하는 PagingAndSortingRepository 인터페이스가 정의되어 있다. 해당 Repository의 메서드에서 Page 정보를 반환하기 위해 Pageable 과 Sort 객체를 사용할 수 있다. Page 객체에는 내부적으로 쿼리를 실행할 때 사용되는 totalElements_와 _totalPages 속성이 포함되어 있다. 이 정보들은 다음 페이지를 요청할 때 사용될 수 있다.
반면에, Spring Data Reactive는 pagination을 완전하게 지원하지는 않는다. 이러한 이유는 Spring Reactive는 비동기 논블로킹을 지원하므로 특정 페이지 크기에 해당하는 모든 데이터가 반환될 때까지 블로킹이 되면 효율적이지 않기 때문이다. 그러나 Spring Data Reacrive는 여전히 Pageable_을 지원한다. _PageRequest 객체를 사용하여 특정 데이터의 청크와 데이터의 개수를 조회하도록 구성할 수 있다.
페이지와 레코드에 대한 메타데이터가 포함된 Spring Data를 사용할 때 Page 대신 Flux로 구성된 응답을 받을 수 있다.
4. WebFlux와 Data Reactive를 활용한 Pagination 예제 코드
Page_와 _Size 정보를 갖고 있는 Pageable 객체를 통해 Repository로부터 페이징된 목록을 조회할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
@Repository
interface NotificationRepository : CoroutineCrudRepository<NotificationEntity, UUID> {
suspend fun findAllByReceiverIdOrderByNotifiedDateDesc(
receiverId: UUID,
pageable: Pageable,
): Flow<NotificationEntity>
suspend fun countByReceiverId(
receiverId: UUID,
): Long
}
- 페이징 정보와 별도로 전체 개수를 조회할 수 있도록 구성함
Repository로부터 페이징된 리스트를 조회하기 위해 다음과 같이 PageRequest.of_를 활용하여 _Pageable 객체를 생성한다.
1
2
3
4
5
6
7
data class GetNotificationsRequest(
val memberId: UUID,
private val page: Int,
private val size: Int,
) {
val pageable: Pageable = PageRequest.of(page, size)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
class NotificationQueryAdapter(
private val notificationRepository: NotificationRepository,
) : LoadNotificationPort {
override suspend fun loadCount(memberId: UUID): Long =
notificationRepository.countByReceiverId(memberId)
override suspend fun loadNotifications(request: GetNotificationsRequest): Flow<Notification> =
notificationRepository
.findAllByReceiverIdOrderByNotifiedDateDesc(request.memberId, request.pageable)
.map { it.toDomain() }
}
그 후 조회된 결과와 전체 개수로 PageImpl 객체를 생성한다.
1
2
3
4
5
6
7
8
9
10
11
12
@Service
class NotificationQueryService(
private val loadNotificationPort: LoadNotificationPort,
) : GetNotificationUseCase {
override suspend fun getNotifications(request: GetNotificationsRequest): Page<NotificationResponse> {
val notifications = loadNotificationPort.loadNotifications(request).map { it.toResponse() }
val count = loadNotificationPort.loadCount(request.memberId)
return PageImpl(notifications.toList(), request.pageable, count)
}
}
Page 정보를 곧바로 응답하면 에러가 발생하므로, 다음과 같이 PageResponse 사용자 정의 객체로 변환하여 응답한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
data class PageResponse<T>(
val data: List<T>,
val pageable: PageableResponse,
) {
companion object {
fun <T> Page<T>.toResponse() =
PageResponse(
data = this.content,
pageable = this.toPageable(),
)
}
data class PageableResponse(
val totalPages: Int,
val totalElements: Long,
) {
companion object {
fun Page<*>.toPageable() =
PageableResponse(
totalPages = this.totalPages,
totalElements = this.totalElements
)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
class NotificationRouter(
private val getMemberUseCase: GetMemberUseCase,
private val getNotificationUseCase: GetNotificationUseCase,
) {
@GetMapping("/notifications")
suspend fun getNotifications(
@RequestHeader(AUTHORIZATION)
authorization: String,
@RequestParam(name = "page", defaultValue = "0")
page: Int,
@RequestParam(name = "size", defaultValue = "10")
size: Int,
): PageResponse<NotificationResponse> {
val memberId: UUID = getMemberUseCase.getMemberIdBy(authorization)
val request = GetNotificationsRequest(memberId, page, size)
val notifications: Page<NotificationResponse> = getNotificationUseCase.getNotifications(request)
return notifications.toResponse()
}
}