TIL(Today I Learned)

TIL - Offset-based Pagination & Cursor-based Pagination

Happy._. 2024. 5. 16. 18:54

오프셋 기반 페이지네이션(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 요청을 보내면 다음과 같은 데이터를 확인할 수 있다.

 

 

참고자료

https://velog.io/@minsangk/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-Cursor-based-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#2-%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-cursor-based-pagination