커서 기반 페이지네이션
커서 기반 페이지네이션은 오프셋 기반 페이지네이션의 한계를 극복하기 위한 방법으로, 마지막으로 읽은 위치를 나타내는 커서 또는 마커를 사용하여 데이터의 연속된 페이지를 효율적으로 검색하는 방식입니다.
커서 기반 페이지네이션은 오프셋 기반 페이지네이션의 한계를 극복하기 위해 커서 또는 마커를 사용하여 마지막으로 읽은 위치를 추적합니다.
커서 기반 페이지네이션에서는 다음과 같은 방식으로 작동합니다.
-
커서는 마지막으로 읽은 키 또는 마커를 받아옵니다.
- 이는 데이터셋 내에서 특정 위치를 나타냅니다. 고유 식별자, 타임스탬프 또는 데이터셋 내 위치를 정의하는 다른 값을 사용할 수 있습니다.
-
오프셋 값 대신 커서를 기반으로 다음 데이터 세트를 검색합니다.
- 이를 통해 스캔하고 폐기할 필요 없이 연속된 페이지를 효율적으로 검색할 수 있습니다.
-
키와 사이즈는 검색할 데이터의 범위를 결정하는 데 사용됩니다.
- 키는 다음 페이지의 시작점을 나타내고, 사이즈는 반환할 레코드 수를 결정합니다.
커서 기반 페이지네이션 구현
Cursor 생성
cursor 키를 생성할 때 중요한 것은 항상 고유 인덱스가 있고, 중복이 없는지 확인하는 것입니다.
public record CursorRequest(Long key, int size) {
public static final Long NONE_KEY = -1L; // 음수를 키로 가질 수 없으니까 사용하자!
public Boolean hasKey() { // 그 다음 함수가 사용할 키를 만들어주는거임
return key != null && !key.equals(NONE_KEY);
}
public CursorRequest next(Long key) {
return new CursorRequest(key, size);
}
}
이제 프로세스를 단계별로 설명하겠습니다
- 커서의 키와 크기를 저장하는 커서 요청 클래스를 만듭니다.
- hasKey() 메서드를 구현하여 커서 요청에 유효한 키가 있는지 확인합니다.
- next() 키를 구현하여 다음 키로 새 커서 요청을 만듭니다
Repository
다음으로 Cursor 키를 사용하여 데이터를 검색하는 쿼리를 고려해야합니다.
이 경우 Cursor 키는 null이 아니어야 합니다.
아래 쿼리는 앞으로 사용될 예시입니다.
select * from POST where memberId = 4 and id > cursor;
레퍼지터리에서는 이를 처리하기 위해 다음 메소드를 정의 할 수 있습니다
public List<Post> findAllByMemberIdInAndOrderByIdDesc(List<Long> memberIds, int size) {
if (memberIds.isEmpty()) {
return List.of();
}
var params = new MapSqlParameterSource()
.addValue("memberIds", memberIds)
.addValue("size", size);
String query = String.format("""
SELECT *
FROM %s
WHERE memberId in (:memberIds)
ORDER BY id DESC
LIMIT :size
""", TABLE);
return namedParameterJdbcTemplate.query(query, params, ROW_MAPPER);
}
Service
서비스 계층에서 repository에서 생성한 메소드를 사용하여 데이터를 검색할 수 있습니다.
public PageCursor<Post> getPosts(Long memberId, CursorRequest cursorRequest) {
var posts = findAllBy(memberId, cursorRequest);
long nextKey = getNextKey(posts);
return new PageCursor<>(cursorRequest.next(nextKey), posts);
}
private long getNextKey(List<Post> posts) {
return posts.stream()
.mapToLong(Post::getId)
.min()
.orElse(CursorRequest.NONE_KEY);
}
private List<Post> findAllBy(Long memberId, CursorRequest cursorRequest) {
if (cursorRequest.hasKey()) {
return postRepository.findAllByLessThanIdAndMemberIdAndOrderByIdDesc(
cursorRequest.key(),
memberId,
cursorRequest.size()
);
}
return postRepository.findAllByMemberIdInAndOrderByIdDesc(List.of(memberId), cursorRequest.size());
}
Controller
마지막으로 컨트롤러에서는 커서 기반 페이지 매김을 사용하여 게시물을 검색하는 엔드포인트를 정의 할 수 있습니다
@GetMapping("/members/{memberId}/by-cursor")
public PageCursor<Post> getPostsByCursor(
@PathVariable Long memberId,
CursorRequest cursorRequest
) {
return postReadService.getPosts(memberId, cursorRequest);
}
부족한 점이나 잘못 된 점을 알려주시면 시정하겠습니다 :>
'DEV > Backend' 카테고리의 다른 글
Fan Out On Write (Push Model) (0) | 2023.06.09 |
---|---|
커버링 인덱스 (0) | 2023.06.09 |
오프셋 기반 페이지네이션 (0) | 2023.06.09 |
페이지네이션 (0) | 2023.06.09 |
조회 최적화를 위한 인덱스 (2) | 2023.06.02 |