입문4분 분량

DynamoDB의 페이지네이션

DynamoDB는 한 번의 호출로 "전체" 결과를 반환하는 법이 없다. QueryScan은 최대 1 MB의 데이터를 반환한 뒤, 이어서 진행할 LastEvaluatedKey를 건네준다. 페이지네이션을 제대로 한다는 것은 카운터가 아니라 그 키를 기준으로 반복한다는 뜻이다.

DynamoDB에서 페이지네이션은 어떻게 동작하나?

QueryScan은 호출당 최대 1 MB를 반환한 다음 LastEvaluatedKey를 돌려준다. 페이지를 넘기려면 그 키를 다음 호출의 ExclusiveStartKey로 전달하고, DynamoDB가 키를 반환하지 않을 때까지 반복한다. 페이지 번호도, 총 개수도 없으며, Limit은 반환된 항목이 아니라 평가된 항목의 상한을 정한다.

let key;
do {
  const out = await client.send(new QueryCommand({...params, ExclusiveStartKey: key}));
  process(out.Items);
  key = out.LastEvaluatedKey;
} while (key);

LastEvaluatedKeyundefined이면 끝에 도달한 것이다. 다음 조각을 가져오려면 그것을 ExclusiveStartKey로 다시 전달하자.

제어 흐름은 키가 없을 때만 빠져나가는 하나의 반복문이다:

있음없음Query / Scan항목 처리LastEvaluatedKey?ExclusiveStartKey 설정완료

매 순회는 반환된 키에서 재개하거나 멈춘다 — 카운터는 없다.

Limit은 페이지 크기가 아니다

Limit은 DynamoDB가 평가하는 항목 수의 상한이지, FilterExpression 이후 반환하는 항목 수가 아니다. 필터 뒤에 있는 Limit: 25 쿼리는 3개의 항목을 반환하면서도 여전히 LastEvaluatedKey를 건넬 수 있다 — 페이지가 짧아 보여도 키가 빌 때까지 계속 넘겨야 한다. 비어 있지 않은 LastEvaluatedKey일치하는 항목이 더 있다고 약속하는 것도 아니다. 없는 키만이 끝에 도달했음을 증명한다.

SDK가 페이지네이션하게 하기

두 SDK 모두 위의 반복문을 감싸므로 페이지를 직접 순회할 수 있다:

// AWS SDK for JavaScript v3
import {paginateQuery} from '@aws-sdk/lib-dynamodb';
for await (const page of paginateQuery({client}, params)) {
  process(page.Items);
}
# boto3
paginator = client.get_paginator('query')
for page in paginator.paginate(**params):
    process(page['Items'])

페이지 번호는 없다

DynamoDB에는 총 개수도 임의 페이지 접근도 없다 — 커서를 다시 재생하지 않고는 "7페이지"로 건너뛰거나 뒤로 넘길 수 없다. 번호 매긴 페이지가 아니라 무한 스크롤 / "더 보기"를 중심으로 UI를 설계하자. (Select: 'COUNT' 쿼리도 개수를 세기 위해 일치한 모든 항목을 읽고 — 청구한다.)

API를 위한 무상태 커서

LastEvaluatedKey는 마지막 항목의 키 속성일 뿐이다. 이를 base64로 인코딩해 불투명한 nextToken으로 클라이언트에 건네고, 다음 요청에서 다시 ExclusiveStartKey로 디코딩하자. 서버 측 커서 상태가 없다.

그 토큰은 DynamoDB-JSON이다 — DynamoDB-JSON 변환기로 직접 살펴보거나 손으로 만들 수 있다. 그리고 Scan을 우회하려고 페이지를 넘기고 있다면, 그건 보통 대신 인덱스를 추가하라는 신호다.

루프 자체를 아예 쓰지 않으려면, 쿼리 빌더가 전체 Query/Scan 요청을 구성하고 페이지네이션 루프까지 포함된 실행 가능한 SDK v3, CLI, boto3 프로그램을 출력한다.

커서가 대신 추적되는 채로 쿼리 결과를 시각적으로 넘겨 보려면 DynoTable을 사용해 보자.

업데이트됨