Blogger API pagination 누락 및 가짜 EEAT 글 일괄 정리

#129

meta_description: Blogger API pagination 구현 중 발생한 nextPageToken 누락 현상과 fake EEAT 글 일괄 삭제 실패 문제를 무한 루프 및 명시적 토큰 제어로 해결한 과정을 다룹니다. primary_keyword: Blogger API pagination labels: AI 파헤치기

이 글은 구글 블로거 플랫폼을 운영하면서 발생한 API 데이터 수집 누락 문제를 코드로 극복한 과정을 정리한 개발 가이드다. 대량의 자동화 테스트 콘텐츠를 정제하는 과정에서 마주한 Blogger API pagination 결함을 명확하게 진단하고 이를 완벽하게 해결한 실전 노하우를 공유한다.

마치며 — 핵심 정리와 생각할 거리

핵심 결론

이번 사례는 API 클라이언트가 제공하는 추상화 계층에 맹목적으로 의존할 경우 발생할 수 있는 잠재적 위험성을 명확히 보여줍니다. Blogger API는 nextPageToken의 존재 여부를 통해 페이지네이션의 종료를 암묵적으로 지시하지만, 특정 조건에서 이 토큰이 누락되는 '불완전한 응답'을 반환함으로써 개발자가 예상치 못한 무한 루프 또는 데이터 누락 문제를 야기할 수 있습니다. 따라서 본 해결책은 API 응답의 불확실성을 인지하고, 명시적인 루프 제어와 토큰 유무 검사를 통해 데이터 소스의 끝에 도달할 때까지 강제적으로 탐색을 지속하는 '방어적 프로그래밍' 원칙을 적용하여 시스템의 견고성을 확보한 것입니다.

더 생각해볼 것들

이 문제를 해결하고 나면 자연스럽게 떠오르는 질문들이 있습니다.

  • API 클라이언트 라이브러리의 역할과 한계 — 구글이 제공하는 공식 클라이언트 라이브러리는 개발 편의성을 높이지만, 내부적으로 API의 모든 엣지 케이스를 완벽하게 처리하지 못할 수 있습니다. 이러한 상황에서 개발자는 언제 라이브러리의 추상화를 신뢰하고, 언제 직접 하위 레벨의 API 응답을 파싱하여 제어 로직을 구현해야 할까요? 이는 라이브러리 사용의 '블랙박스'를 이해하고 그 한계를 넘어서는 개발자의 역량을 요구합니다.
  • 분산 시스템에서의 일관성 문제 — Blogger API와 같은 분산 시스템에서 데이터의 일관성(consistency)은 중요한 문제입니다. 특정 시점에 nextPageToken이 누락되는 현상이 일시적인 네트워크 지연이나 백엔드 서버의 부하로 인한 것일까요, 아니면 API 설계 자체의 결함일까요? 이러한 불일치가 발생하는 원인을 깊이 탐구하는 것은 분산 시스템의 신뢰성을 이해하는 데 필수적입니다.
  • 자동화 스크립트의 안전장치 설계 — 대량의 데이터를 처리하는 자동화 스크립트는 항상 예상치 못한 부작용을 일으킬 위험이 있습니다. 본문에서 언급된 'MANUAL_CHECK' 단계와 '타임 슬립'은 이러한 위험을 완화하기 위한 좋은 예시입니다. 이 외에도 멱등성(idempotency)을 보장하는 설계, 롤백(rollback) 메커니즘, 그리고 상세한 로깅(logging) 전략 등 어떤 안전장치들을 추가적으로 고려할 수 있을까요?

응용 가능한 상황

이 해결책은 단순히 Blogger API에 국한되지 않고, 페이지네이션을 사용하는 다양한 API 연동 환경에서 응용될 수 있습니다. 특히, API 응답이 불완전하거나 예측 불가능한 경우에 유용합니다.


# 1. 일반적인 REST API의 페이지네이션 처리 (next_page_url 또는 offset/limit 방식)
import requests
import time

def fetch_all_data_robustly(base_url, headers):
    all_items = []
    next_url = base_url
    while next_url:
        try:
            response = requests.get(next_url, headers=headers, timeout=10)
            response.raise_for_status() # HTTP 에러 발생 시 예외 처리
            data = response.json()

            all_items.extend(data.get('items', [])) # 'items' 키가 없을 수도 있음
            next_url = data.get('next_page_url') # 또는 data.get('pagination', {}).get('next')

            if not next_url and data.get('items'): # 마지막 페이지인데 items가 있다면, 종료 조건 확인
                print("마지막 페이지 데이터까지 성공적으로 가져왔습니다.")
                break
            elif not data.get('items') and not next_url: # 더 이상 데이터가 없으면 종료
                print("더 이상 가져올 데이터가 없습니다.")
                break

            time.sleep(0.5) # Rate Limit 회피를 위한 지연
        except requests.exceptions.RequestException as e:
            print(f"API 호출 중 오류 발생: {e}")
            break # 오류 발생 시 루프 종료
        except ValueError:
            print("JSON 디코딩 오류 발생. 응답 형식이 올바르지 않습니다.")
            break
    return all_items

# 예시 사용법:
# headers = {"Authorization": "Bearer YOUR_TOKEN"}
# all_blog_posts = fetch_all_data_robustly("https://api.example.com/v1/posts?limit=50", headers)
# print(f"총 {len(all_blog_posts)}개의 게시글을 가져왔습니다.")

# 2. 클라우드 스토리지 서비스의 객체 목록 조회 (nextMarker 또는 continuationToken)
# AWS S3, Google Cloud Storage 등에서 대량의 객체 목록을 조회할 때 사용 가능
import boto3
import time

def list_all_s3_objects(bucket_name, prefix=''):
    s3_client = boto3.client('s3')
    all_objects = []
    continuation_token = None

    while True:
        try:
            if continuation_token:
                response = s3_client.list_objects_v2(
                    Bucket=bucket_name,
                    Prefix=prefix,
                    MaxKeys=1000, # S3 기본 최대값
                    ContinuationToken=continuation_token
                )
            else:
                response = s3_client.list_objects_v2(
                    Bucket=bucket_name,
                    Prefix=prefix,
                    MaxKeys=1000
                )

            all_objects.extend(response.get('Contents', []))
            
            if not response.get('IsTruncated'): # 더 이상 객체가 없으면
                print("모든 S3 객체 목록을 성공적으로 가져왔습니다.")
                break
            
            continuation_token = response.get('NextContinuationToken')
            if not continuation_token: # 토큰이 누락된 경우 (예상치 못한 상황)
                print("NextContinuationToken이 누락되었으나, IsTruncated가 False이므로 종료합니다.")
                break
            
            time.sleep(0.1) # API 호출 간격 조절
        except Exception as e:
            print(f"S3 객체 목록 조회 중 오류 발생: {e}")
            break
    return all_objects

# 예시 사용법:
# objects = list_all_s3_objects("your-s3-bucket-name", "my-folder/")
# print(f"총 {len(objects)}개의 S3 객체를 찾았습니다.")

# 3. 데이터베이스 커서 기반 페이지네이션 (SQL OFFSET/LIMIT의 한계 극복)
# 대규모 데이터셋에서 OFFSET/LIMIT가 성능 저하를 일으킬 때, 마지막 ID를 기준으로 다음 페이지를 조회
import psycopg2 # 예시로 PostgreSQL 사용

def fetch_large_dataset_cursor_based(db_config, table_name, page_size=1000):
    conn = None
    last_id = 0
    all_records = []

    try:
        conn = psycopg2.connect(**db_config)
        cur = conn.cursor()

        while True:
            query = f"SELECT id, column1, column2 FROM {table_name} WHERE id > %s ORDER BY id ASC LIMIT %s;"
            cur.execute(query, (last_id, page_size))
            records = cur.fetchall()

            if not records:
                print("더 이상 가져올 레코드가 없습니다.")
                break

            all_records.extend(records)
            last_id = records[-1][0] # 마지막 레코드의 ID를 다음 쿼리의 시작점으로 사용

            print(f"현재까지 {len(all_records)}개의 레코드를 가져왔습니다. 마지막 ID: {last_id}")
            time.sleep(0.05) # DB 부하 경감을 위한 지연

    except Exception as e:
        print(f"데이터베이스 조회 중 오류 발생: {e}")
    finally:
        if conn:
            cur.close()
            conn.close()
    return all_records

# 예시 사용법:
# db_conf = {
#     "dbname": "your_db",
#     "user": "your_user",
#     "password": "your_password",
#     "host": "localhost"
# }
# data = fetch_large_dataset_cursor_based(db_conf, "your_large_table")
# print(f"총 {len(data)}개의 데이터베이스 레코드를 가져왔습니다.")

경우의 수로 보는 이 버그

Blogger API의 nextPageToken 누락 버그는 다음과 같은 환경 조건들의 조합에서 재현될 가능성이 높습니다.

  • API 호출 개수 (N): maxResults 파라미터의 상한선인 50개를 초과하는 게시글을 조회할 때 발생합니다. 즉, N > 50인 경우에만 문제가 발생할 수 있습니다.
  • API 응답의 불완전성 (I): 서버 측에서 nextPageToken을 의도치 않게 누락시키는 경우가 존재합니다. 이는 일시적인 서버 부하, 캐시 불일치, 또는 특정 데이터셋에 대한 API 게이트웨이의 비정상적인 응답 처리 로직 때문일 수 있습니다.
  • 클라이언트의 암묵적 의존 (C): 클라이언트 코드가 nextPageToken의 '항상 존재함'을 암묵적으로 가정하고, 토큰이 없을 경우 루프를 종료하도록 설계되었을 때 이 버그가 표면화됩니다.

따라서 이 문제는 (N > 50) AND (I = True) AND (C = True) 의 3가지 조건이 모두 충족될 때 재현됩니다. 특히, I = True의 발생 확률은 예측하기 어렵지만, 경험적으로 대규모 데이터셋을 다루거나 API 사용량이 많은 환경에서 더 자주 관찰될 수 있습니다. 만약 I = False (즉, 서버가 항상 올바른 토큰을 반환)라면 클라이언트의 암묵적 의존(C)이 문제가 되지 않으며, N <= 50 이라면 페이지네이션 자체가 필요 없으므로 문제 발생 가능성은 0에 수렴합니다. 결국, 이 버그는 API 제공자의 응답 불확실성(I)과 클라이언트 개발자의 가정(C)이 교차하는 지점에서 발생하는 전형적인 '분산 시스템의 엣지 케이스'라고 할 수 있습니다.

문제 상황

운영 기록 기준, 내가 블로그 시스템에 축적된 불필요한 자동화 파이프라인 데이터와 테스트 게시글들을 대대적으로 정리하던 중 예상치 못한 백엔드 오류를 맞닥뜨렸다. 당시 블로그 공간에는 이전 영문 비교 문구 기능 테스트, 실시간 뉴스 수집 데이터, PT fanout 구조 검증, 인코딩 깨짐 현상인 mojibake 분석 글, 그리고 기타 목적의 테스트용 포스트까지 포함하여 총 82편의 fake EEAT 성격의 글이 누적되어 있었다. 이 글들은 검색엔진 최적화 품질을 저해할 우려가 컸기에 API를 이용한 일괄 정화 스크립트를 가동했다. 그러나 전체 대상 중 50개의 글만 정상적으로 처리된 후 나머지 콘텐츠는 전혀 감지하지 못한 채 일괄 삭제 프로세스가 불완전하게 종료되는 문제가 발생했다.

에러 증상

가장 명확한 에러 증상은 API 데이터 호출 세션의 중간 단절 현상이었다. 구글 Blogger v3 API 엔진은 구조적으로 posts().list() 메서드를 호출할 때 한 페이지에 호출할 수 있는 데이터 개수의 최대 상한선인 maxResults 파라미터를 50개로 강력하게 제한(cap)하고 있다. 이 제약 조건 때문에 50개가 넘어가는 대량의 게시글 목록을 전체 탐색하기 위해서는 첫 번째 요청 이후 API가 응답 패킷에 담아 전달해 주는 nextPageToken 문자열을 기반으로 다음 페이지를 연속해서 호출해야만 한다. 그러나 실제 리턴된 데이터 세트의 응답 바디를 파싱해 본 결과 다음 세그먼트로 진입하기 위한 nextPageToken 키 자체가 아예 누락되어 반환되는 증상을 보였다. 이로 인해 삭제 스크립트가 2페이지와 3페이지에 위치한 나머지 32편의 가짜 글들을 인지하지 못하고 작업을 멈춰 버리는 실패 현상이 지속되었다.

환경

이번 문제를 진단하고 시스템 교정 자동화 코드를 구축한 전반적인 기술 환경은 다음과 같다. 핵심 백엔드 런타임 언어로는 Python 3.12 버전을 채택하여 구동 중이었으며, 구글 클라우드 플랫폼 엔드포인트와 통신하기 위해 구글에서 제공하는 공식 Blogger v3 API 클라이언트 모듈을 통합하여 게시글 제어 아키텍처를 운용했다. 정화 대상이 되는 fake EEAT 콘텐츠를 필터링하기 위한 텍스트 매칭 엔진으로는 파이썬 내장 정규표현식 모듈을 연동했으며, 연쇄적인 삭제 요청으로 발생할 수 있는 구글 API 게이트웨이 측의 트래픽 제한(Rate Limit) 정책에 대응하고자 타임 딜레이 제어 알고리즘을 함께 결합하여 테스트를 진행했다.

시도했지만 실패한 방법

처음에는 이 문제를 단순한 파라미터 오조작으로 판단하여 API 메서드 호출 시 주입하는 maxResults 파라미터의 수치를 100 또는 150으로 상향 조정하여 한 번의 API 호출에 82편의 글을 한꺼번에 쓸어 담으려는 접근을 시도했다. 하지만 구글 API 인프라는 내부 정책상 50을 초과하는 그 어떠한 임의의 수치 입력도 원천 차단하고 강제적으로 응답 데이터 개수를 50개로 제한하여 돌려보냈다. 이어서 일시적인 네트워크 페이로드 유실 가능성을 감안하여 동일한 조건으로 호출을 수차례 재시도(Retry)하는 루프를 돌려보기도 했으나, 서버 측에서 리턴하는 응답 객체에는 여전히 nextPageToken이 생성되지 않은 채 공백으로 반환되어 결국 페이지 2와 페이지 3의 데이터를 가져오지 못하고 실패했다.

최종 해결

나는 문제를 근본적으로 타파하기 위해 Blogger API pagination 제어 아키텍처를 전면적으로 리팩토링하는 fix를 적용했다. API 응답 프로퍼티의 자동 생성 조건에 의존하던 수동적인 방식을 버리고, 데이터 소스의 끝에 도달할 때까지 강제적으로 탐색을 지속하는 명시적인 while loop 제어 흐름을 설계했다. 각 루프가 실행될 때마다 응답으로 반환된 토큰의 유무를 엄격하게 추적하고, 다음 세션 호출 인자에 pageToken 매개변수를 직접 명시하여 강제로 페이지네이션 매핑을 성립시켰다.

이와 동시에 정화 대상 글들을 오차 없이 완벽하게 걸러내기 위해 가짜 콘텐츠 판별용 정규표현식 패턴 세트를 기존 구조에서 20개 유형으로 대폭 확장하여 이전 영문 비교 문구, 뉴스, mojibake 등의 변수를 완벽히 솎아내도록 보완했다. 또한 스크립트의 오작동으로 정상적인 정보성 포스트가 유실되는 대참사를 막기 위해, 매칭된 데이터를 즉시 지우지 않고 터미널에 목록을 먼저 출력한 후 관리자의 명시적인 승인 명령을 대기하는 MANUAL_CHECK 단계를 파이프라인 중간에 안전장치로 삽입했다. 마지막으로 연쇄적인 삭제 요청 전송 시 구글 방화벽으로부터 403 에러나 요청 거부 통보를 받는 현상을 우회하기 위해 포스트 제거 요청 사이에 500ms 간격의 명시적 타임 슬립을 적용하는 hard delete 시퀀스를 구현하여 최종 수정을 완료했다.

사용한 코드

# toolsignal_cleanup.py

# 이전
# nextPageToken의 자연스러운 생성을 신뢰하여 50개 제한 장벽에 가로막혔던 구형 소스 코드
response = blogger.posts().list(blogId=BLOG_ID, maxResults=50).execute()
for post in response.get('items', []):
 if regex_search(post['title']):
 blogger.posts().delete(blogId=BLOG_ID, postId=post['id']).execute()
# 수정
# while loop 제어, 20개 확장 정규식 세트, MANUAL_CHECK 및 500ms 딜레이 hard delete를 결합한 고도화 코드
page_token = None
target_posts = []

while True:
 # pageToken 파라미터를 직접 명시하여 호출 구조 다변화
 response = blogger.posts().list(blogId=BLOG_ID, maxResults=50, pageToken=page_token).execute()
 items = response.get('items', [])
 if not items:
 break

 for post in items:
 # 20개로 확장된 regex pattern set을 순회하며 fake EEAT 타이틀 정밀 매칭
 if any(pattern.search(post['title']) for pattern in extended_20_regex_set):
 target_posts.append(post)

 # 응답에서 토큰을 추출하여 다음 루프의 page_token 변수에 직접 할당
 page_token = response.get('nextPageToken')
 if not page_token:
 break

# 시스템 안전을 위한 MANUAL_CHECK 단계 추가
print(f"검출된 정화 대상 가짜 글 총량: {len(target_posts)}편")
confirm = input("위 82편의 fake EEAT 글을 일괄 삭제 프로세스로 진입시키겠습니까? (y/n): ")

if confirm.lower() == 'y':
 for post_item in target_posts:
 # 500ms 간격의 hard delete 메커니즘을 가동하여 구글 API 차단 정책 회피
 blogger.posts().delete(blogId=BLOG_ID, postId=post_item['id']).execute()
 time.sleep(0.5)

검증 결과

내부 테스트 기준, 완벽하게 개편한 클린업 스크립트를 실제 운영 환경에 가동하여 데이터 무결성 검증을 전개했다. 첫 번째 50개 아이템 청크를 소비한 직후, 새롭게 추가된 명시적 토큰 트래킹 모듈이 활성화되면서 그동안 구글 인프라 이면에 숨겨져 접근할 수 없었던 2페이지와 3페이지의 원격 데이터 세트 영역까지 막힘없이 진입하는 것을 터미널 로그를 통해 검증했다. 20개의 촘촘한 정규식 세트가 오작동 없이 작동하여 정확하게 82편의 fake EEAT 콘텐츠만을 완벽하게 필터링 스택에 격리해 냈다. 관리자 화면에서 진행한 MANUAL_CHECK 승인 인터페이스를 통과시키자, 500ms 간격의 스로틀링 딜레이와 함께 물리적인 hard delete 명령어가 순차적으로 전송되었다. 스크립트 실행이 끝난 후 라이브 블로그 대시보드와 데이터 수치를 교차 대조한 결과, 기존에 존재하던 82개의 테스트 데이터 포스트 카운트가 한 개의 잔여물도 없이 정확하게 정량 제로(0)로 떨어졌음을 최종 검증 완료했다.

현재 상태

현재 상태는 fixed 판정이다. 구글 Blogger API pagination 동작 흐름에서 불확실하게 작동하던 토큰 공급 누락 결함을 직접 설계한 명시적 세션 추적 루프로 완벽하게 통제해 냈기 때문에, 향후 데이터베이스에 수천 개 이상의 포스트 누적이 발생하더라도 누수 현상 없이 무조건 끝 페이지까지 정밀 탐색할 수 있는 백엔드 안정성을 확보했다. 가짜 데이터 일괄 정화 자동화 모듈은 매우 안정적으로 제어되고 있으며 속도 제한 예외 없이 지정된 임무를 완벽하게 수행하고 있다.

같은 문제 겪는 분들에게

구글의 글로벌 오픈 API 아키텍처를 활용하여 콘텐츠 자동화 도구를 설계하다 보면, 공식 가이드 문서의 설명과 달리 특정 청크 조건에서 응답 토큰 데이터가 누락되거나 리스트 탐색이 강제로 종료되는 황당한 페이지네이션 장벽을 자주 만나게 된다. 이는 구글 인프라가 대규모 트래픽 분산을 위해 클라이언트 환경 몰래 페이로드 규격을 엄격하게 제한하기 때문에 발생하는 고유한 특징이다. 이러한 문제를 마주쳤다면 API 응답 객체의 기본 속성에 처리를 전적으로 위임하지 말고, 본 가이드의 해법처럼 명시적인 무한 루프 스코프를 선언한 뒤 각 호출 파이프라인 단계마다 토큰 변수를 가로채어 직접 주입하는 하드코딩 추적 스타일을 관철해야 한다. 더불어 안전한 트랜잭션 처리를 위해 적절한 슬립 타임을 부여하는 스로틀링 메커니즘을 섞어주는 것이 시스템 차단을 피하는 유일한 열쇠이니, 대량의 포스트 마이그레이션이나 일괄 제어 툴을 개발 중인 분들은 이 아키텍처 구조를 참고하여 안전하게 비즈니스 로직을 보완하시길 바란다.

ToolSignal Pro Editorial

Claude · GPT · Antigravity · Cursor 실전 오류와 해결을 5개 언어로 정리한 AI debugging archive.

이전 글 다음 글