오프셋 기반 페이지네이션(Offset-based Pagination)
- DB의 offset, limit 쿼리를 사용
- 이전의 데이터를 모두 조회 후 offset을 조건으로 잘라내는 방식
- 페이지 단위로 구분해 요청 및 응답
- 총 레코드 개수 및 전체 페이지 크기를 알 수 있음
- 원하는 페이지로 바로 이동 가능
- 데이터의 변경이 잦은 경우 중복 또는 누락 데이터가 발생(데이터 불일치)
- 레코드의 개수가 많고 offset 값이 올라갈수록 시간 복잡도 ↑
SELECT * FROM todo ORDER BY create_at DESC LIMIT 10 OFFSET 10 -- 10개 skip 후 10개의 데이터 요청
SELECT * FROM todo ORDER BY create_at DESC LIMIT 20 OFFSET 10 -- 20개 skip 후 10개의 데이터 요청
SELECT * FROM todo ORDER BY create_at DESC LIMIT 30 OFFSET 10 -- 30개 skip 후 10개의 데이터 요청
커서 기반 페이지네이션(Cursor-based Pagination)
- Cursor 개념을 사용
- 사용자에게 응답한 마지막 데이터 기준 다음 N개를 요청 및 응답
- 데이터 추가, 삭제에 안정적(데이터 중복, 누락 발생 X)
- 데이터의 양이 많아질수록 offset에 비해 더 효율적
- 사용자가 원하는 페이지로 바로 이동할 수 없음(총 데이터의 개수를 알 수 없음)
- Cursor는 고유하고 순차적인 열을 사용, 아니면 일부 데이터를 건너뛸 수 있음
- 정렬 기능의 제한(고유 하지 않은 중복 데이터로 정렬해야 하는 경우, 시간 복잡도 ↑, 구현의 어려움)
SELECT * FROM todo WHERE id < 20 ORDER BY id DESC LIMIT 10; -- id가 20미만인 데이터 10개
SELECT * FROM todo WHERE id < 10 ORDER BY id DESC LIMIT 10; -- id가 10미만인 데이터 10개
JPA로 Offset-based Pagination 구현
Controller
@RestController
@RequestMapping("/todos")
class TodoController(
private val todoService: TodoService
) {
@GetMapping
fun getTodoList(
@PageableDefault(page = 0, size = 10, sort = ["id"], direction = Sort.Direction.DESC) pageable: Pageable
): ResponseEntity<Page<TodoResponse>> {
return ResponseEntity.status(HttpStatus.OK).body(todoService.getAllTodoList(pageable))
}
}
@PageableDefault()는 요청 시 파라미터 값을 보내지 않는 경우 기본값으로 사용된다.
Service
interface TodoService {
fun getAllTodoList(pageable: Pageable): Page<TodoResponse>
}
@Service
class TodoServiceImpl(
private val todoRepository: TodoRepository
) : TodoService {
override fun getAllTodoList(pageable: Pageable): Page<TodoResponse> {
return todoRepository.findAll(pageable).map { it.toResponse() }
}
}
Repository
interface TodoRepository : JpaRepository<Todo, Long> {}
다음과 같이 http://localhost:8080/todos?page=0&size=10&sort=createdAt로 GET 요청을 보내면 createdAt 기준 오름차순 정렬된 10개의 데이터를 받을 수 있다.
내림차순으로 정렬된 데이터를 받고 싶다면 http://localhost:8080/todos?page=0&size=10&sort=createdAt,desc와 같이 마지막에 ,(쉼표)와 desc(내림차순)을 추가한다.
10개의 데이터 다음에 오는 pageable을 보면 다음과 같은 값들을 확인할 수 있다.
- pageNumber : 현재 페이지
- pageSize : 한 페이지 당 데이터 개수
- sort : 정렬 여부
- first : 첫 페이지이면 true, 아니면 false
- last : 마지막 페이지이면 true, 아니면 false
- totalPages : 총 페이지 수
- totalElements : 전체 레코드 개수
- numberOfElements : 데이터 개수
- empty : 데이터가 있으면 true, 없으면 false
JPA로 Cursor-based Pagination 구현
Controller
offset과의 차이는 @RequestParam cursor: Long = 0가 추가된 점이 다름
@RestController
@RequestMapping("/todos")
class TodoController(
private val todoService: TodoService
) {
@GetMapping
fun getTodoList(
@RequestParam cursor: Long = 0,
@PageableDefault(page = 0, size = 10, sort = ["id"], direction = Sort.Direction.DESC) pageable: Pageable
): ResponseEntity<Page<TodoResponse>> {
return ResponseEntity.status(HttpStatus.OK).body(todoService.getAllTodoList(cursor, pageable, writer))
}
}
Service
interface TodoService {
fun getAllTodoList(cursor: Long, pageable: Pageable, writer: String?): Page<TodoResponse>
}
@Service
class TodoServiceImpl(
private val todoRepository: TodoRepository
) : TodoService {
override fun getAllTodoList(cursor: Long, pageable: Pageable, writer: String?): Page<TodoResponse> {
if (writer != null) {
return todoRepository.findNextPage(cursor, pageable).map { it.toResponse() }
} else {
return todoRepository.findNextPage(cursor, pageable).map { it.toResponse() }
}
}
}
Repository
interface TodoRepository : JpaRepository<Todo, Long> {
@Query("select t from Todo t where t.id < :cursor") // 정렬에 따라 <, >, 처리 필요(동적 쿼리 작성 필요)
fun findNextPage(cursor: Long, pageable: Pageable) : Page<Todo>
}
다음과 같이 http://localhost:8080/todos?cursor=6&size=3&sort=id,desc로 GET 요청을 보내면 다음과 같은 데이터를 확인할 수 있다.
참고자료
'TIL(Today I Learned)' 카테고리의 다른 글
TIL - Java 8 date/time type `java.time.LocalDateTime` not supported (0) | 2024.05.20 |
---|---|
TIL - Spring Security에서 JSON으로 로그인 처리 (0) | 2024.05.17 |
TIL - SQL WITH 재귀 쿼리 (0) | 2024.05.14 |
TIL - 댓글을 가져올 때 순환 참조 발생 (0) | 2024.05.13 |
JPA(Java Persistence API)의 이해 2 (0) | 2024.05.10 |