파이썬 API 페이지 수집 종료 조건: 빈 페이지와 반복 커서 검사
API 문서가 정한 다음 페이지 신호로 종료를 판단하고, 이미 사용한 커서와 최대 페이지 수를 함께 검사해야 합니다.
적용 환경: Python 3.14 표준 라이브러리. 2026-10-21 확인. Windows의 Python 3.14.4에서 아래 합성 예제를 임시 작업 폴더로 실행하고 assert 검사를 통과했습니다. 외부 서비스 호출이나 실제 회사 파일 처리는 실행하지 않았습니다.
적은 결과 수는 종료 신호와 같지 않습니다
업무용 API에서 페이지당 100개를 요청했는데 3개만 왔다고 반드시 마지막 페이지인 것은 아닙니다. Google의 AIP-158은 요청 수보다 적은 결과, 심지어 0개를 반환해도 후속 페이지가 있을 수 있다고 설명합니다. 따라서 해당 규약에서는 다음 페이지 토큰을 봐야 합니다. 모든 API가 이 규약을 쓰는 것은 아니며, 실제 엔드포인트 문서가 우선입니다.
가상 응답의 계약을 먼저 고정합니다
이 예제의 fetch_page(token)은 items 목록과 next_token 필드를 가진 사전을 반환합니다. 첫 요청은 None, 종료 신호도 next_token의 None으로 정했습니다. 비어 있지 않은 문자열은 다음 커서입니다. 이는 테스트용 내부 형식이며 Google이나 GitHub의 원래 응답 형식이 아닙니다. 실제 API의 누락 필드·빈 문자열 종료 규칙을 확인한 어댑터에서 이 형식으로 바꿔야 합니다.
직접 실행할 최소 예제
def collect_pages(fetch_page, max_pages=10):
if type(max_pages) is not int or max_pages < 1:
raise ValueError("max_pages는 양의 정수여야 합니다")
token = None
seen = set()
result = []
for _ in range(max_pages):
if token in seen:
raise RuntimeError("페이지 토큰 반복")
seen.add(token)
page = fetch_page(token)
if not isinstance(page, dict) or "items" not in page or "next_token" not in page:
raise ValueError("응답 필드 누락")
items, next_token = page["items"], page["next_token"]
if not isinstance(items, list):
raise ValueError("items는 목록이어야 합니다")
if next_token is not None and (not isinstance(next_token, str) or not next_token):
raise ValueError("next_token은 비어 있지 않은 문자열 또는 None이어야 합니다")
result.extend(items)
if next_token is None:
return result
token = next_token
raise RuntimeError("최대 페이지 수 초과: 불완전한 결과는 반환하지 않습니다")
def expect_error(error_type, fetch, **kwargs):
try:
collect_pages(fetch, **kwargs)
except error_type:
return
raise AssertionError("예상한 오류가 발생하지 않았습니다")
# 직접 만든 가상 응답: 실제 네트워크 요청은 없습니다.
pages = {
None: {"items": ["문서-A"], "next_token": "cursor-a"},
"cursor-a": {"items": [], "next_token": "cursor-b"},
"cursor-b": {"items": ["문서-B"], "next_token": None},
}
assert collect_pages(pages.__getitem__) == ["문서-A", "문서-B"]
assert collect_pages(lambda _: {"items": [], "next_token": None}) == []
expect_error(RuntimeError, lambda _: {"items": [], "next_token": "same"})
expect_error(RuntimeError, pages.__getitem__, max_pages=2)
expect_error(ValueError, lambda _: {"items": []})
expect_error(ValueError, lambda _: {"items": {}, "next_token": None})
expect_error(ValueError, lambda _: {"items": [], "next_token": ""})
expect_error(ValueError, pages.__getitem__, max_pages=0)
cycle = {None: "a", "a": "b", "b": "a"}
expect_error(RuntimeError, lambda t: {"items": [], "next_token": cycle[t]})
print("3페이지(중간 빈 페이지 포함) -> ['문서-A', '문서-B']")
print("빈 결과·반복/순환 토큰·페이지 한도·응답 형식 검사 통과")
검증한 출력
3페이지(중간 빈 페이지 포함) -> ['문서-A', '문서-B'] 빈 결과·반복/순환 토큰·페이지 한도·응답 형식 검사 통과
예상 결과를 정하고 실패 경로도 실행합니다
합성 입력은 문서-A가 있는 첫 페이지, 결과가 비어 있지만 다음 커서가 있는 둘째 페이지, 문서-B가 있는 마지막 페이지입니다. 정상 결과는 두 문서입니다. 같은 커서가 반복되거나 a→b→a로 순환하면 예외를 냅니다. 최대 페이지 수에 닿았는데 종료 신호가 없으면 불완전한 목록을 성공 결과로 돌려주지 않습니다.
실제 API에 붙일 때의 어댑터
Google AIP의 next_page_token과 GitHub Link 헤더의 rel="next"는 서로 다른 인터페이스입니다. GitHub 예제에 이 함수의 필드명을 그대로 찾게 하면 실패합니다. 링크를 따라가는 방식이라면 문서가 제공한 다음 URL을 사용하되, 인증 헤더가 엉뚱한 호스트로 전달되지 않도록 허용 HTTPS 호스트를 검증하세요. 여기서는 외부 HTTP나 인증 동작을 실행하지 않았습니다.
재시도와 페이지 진행은 분리하세요
커서는 불투명한 값으로 취급하고 내부 숫자를 추측해 증가시키지 않습니다. 같은 수집 작업의 검색 조건과 정렬 등은 해당 API가 요구하는 일관성을 유지하세요. HTTP 타임아웃·429·서버 오류 재시도는 fetch_page 내부의 제한된 재시도 정책에서 처리하고, 실패한 요청 때문에 다음 토큰으로 건너뛰면 안 됩니다. max_pages는 단일 요청의 네트워크 타임아웃을 대신하지 않습니다.
이 예제가 보장하지 않는 것
유효한 새 토큰이 계속 오더라도 원본 데이터가 수집 중 바뀌면 중복이나 누락이 생길 수 있습니다. 이 코드는 레코드를 자동 중복 제거하지 않고 순서대로 모읍니다. 고유 ID와 수정 시각, 스냅샷 지원 여부를 확인한 별도 저장 정책이 필요합니다. 결과 전체와 seen 집합을 메모리에 보관하므로 큰 수집에는 체크포인트와 저장소 설계가 필요합니다. 토큰·인증값·회사 응답 원문은 공개 로그에 남기지 마세요.
자주 묻는 질문
items가 빈 목록이면 중단하면 안 되나요?
서비스가 그렇게 명시한 경우에만 가능합니다. 이 예제에서는 다음 토큰이 있으면 빈 페이지도 통과하고 계속 진행합니다.
반복 커서를 만나면 처음부터 자동 재실행하나요?
이 코드는 예외로 중단합니다. 토큰 만료·필터 변경·서버 응답 오류를 확인한 뒤 재시작 범위를 결정해야 중복 저장을 피할 수 있습니다.
공식 문서와 확인일
아래 공식 문서를 2026-10-21에 직접 확인해 예제의 동작과 구분 기준을 대조했습니다. 문서가 설명하는 서비스 규약과 이 글의 가상 입력 규약은 구분해서 적용하세요.
함께 보면 좋은 글
작성 안내: AI 보조로 작성했으며, 제시한 합성 자료와 로컬 코드의 실행 결과를 검증했습니다. 운영자의 실제 업무 경험이나 사람 전문가의 검수를 주장하지 않습니다. 환경과 입력 계약이 달라지면 자체 테스트를 추가하세요.
댓글
댓글 쓰기