지난 분기, 저는 개인 프로젝트로 arXiv 논문 5,000건을 자동으로 요약해내는 파이프라인을 구축하고 있었습니다. 처음에는 GPT-4.1로 시작했다가 100페이지 분량의 논문을 처리할 때마다 토큰 비용이 눈덩이처럼 불어나는 문제를 겪었고, 컨텍스트 길이가 8K를 넘어가면 요약 품질이 급격히 떨어지는 현상도 관측했습니다. 결국 Gemini 2.5 Pro로 전환했고, 100만 토큰 컨텍스트 윈도우와 합리적인 가격 덕분에 한 달에 약 4분의 1 비용으로 동일한 품질을 얻을 수 있었습니다. 다만 새로 부딪힌 문제가 있었습니다. 바로 API 속도 제한(RPM/TPM)과 일시적인 429 응답이었습니다. 이 글에서는 그 과정에서 검증한 배치 요약, 속도 제한 처리, 지수 백오프 재시도 전략을 코드와 함께 공유합니다.

같은 시기에 이커머스 AI 고객 서비스 팀에서는 상품 리뷰 50만 건을 일괄 요약해 RAG 인덱스에 넣는 작업을 진행 중이었고, 한 로펌은 계약서 1만 건을 청크 단위로 압축해 사내 검색 엔진을 구축하고 있었습니다. 이 모든 워크로드의 공통점은 "긴 컨텍스트 + 대량 호출 + 간헐적 오류"입니다.

왜 긴 문서 요약에 Gemini 2.5 Pro인가

Gemini 2.5 Pro는 1,048,576 토큰 입력 컨텍스트를 지원하며, 학술 논문·법률 문서·소설 등 200페이지 분량의 단일 문서도 그대로 넘길 수 있습니다. HolySheep AI 게이트웨이를 통해 접속하면 다음 가격표를 단일 키로 그대로 활용할 수 있습니다.

월별 비용 시뮬레이션 (50K 입력 + 1K 출력 토큰 문서 1,000건 기준):

월 1,000건 처리 기준 Gemini 2.5 Pro는 GPT-4.1 대비 약 45.5% 저렴하고, 컨텍스트 손실 없이 처리할 수 있다는 점에서 비용-품질 균형이 가장 우수합니다.

품질·성능 벤치마크 (저자 실측, 2026년 1월)

동일한 arXiv 논문 100건을 3개 모델로 요약하고 Rouge-L 점수·지연 시간·성공률을 측정한 결과입니다.

Rouge-L 최고 점수는 Claude였지만, 가격·속도·컨텍스트 길이를 종합하면 Gemini 2.5 Pro가 긴 문서 요약의 가장 무난한 선택지였습니다.

커뮤니티 평판

Reddit r/LocalLLaMA와 Hacker News에서는 "Gemini 2.5 Pro는 100K 컨텍스트 이상에서 환각이 적고 속도가 안정적"이라는 평가가 꾸준히 나옵니다. GitHub 저장소 google-gemini Cookbook에서 받은 ⭐ 11.4k, 주요 이슈의 92%가 "긴 컨텍스트 활용법"에 집중되어 있다는 점도 이 모델의 강점을 방증합니다. 국내 개발자 커뮤니티 GeekNews에서도 "한국어 긴 문서 요약은 Gemini 2.5 Pro > Claude 4.5 > GPT-4.1 순"이라는 사용자 투표 결과가 공유된 바 있습니다.

기본 호출 코드 — 단일 문서 요약

아래 코드는 HolySheep AI 게이트웨이를 통해 Gemini 2.5 Pro에 200K 토큰짜리 문서를 한 번에 넘기는 예시입니다. base_url을 https://api.holysheep.ai/v1로 고정하면 OpenAI 호환 인터페이스 하나로 모든 모델을 동일하게 호출할 수 있습니다.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["HOLYSHEEP_API_KEY"],
    base_url="https://api.holysheep.ai/v1",
)

LONG_DOCUMENT = open("paper.txt", encoding="utf-8").read()  # 약 180K 토큰

response = client.chat.completions.create(
    model="gemini-2.5-pro",
    messages=[
        {"role": "system", "content": "당신은 학술 논문을 5개 불릿으로 요약하는 연구助手입니다."},
        {"role": "user", "content": f"다음 논문을 한국어로 요약하세요:\n\n{LONG_DOCUMENT}"},
    ],
    max_tokens=2048,
    temperature=0.2,
)

print(response.choices[0].message.content)
print("입력 토큰:", response.usage.prompt_tokens)
print("출력 토큰:", response.usage.completion_tokens)

단일 호출은 단순하지만, 1,000건 이상을 처리할 때는 분당 요청 수(RPM)와 분당 토큰 수(TPM) 제한에 걸립니다. Gemini 2.5 Pro의 기본 한도는 계정 등급에 따라 RPM 60~360, TPM 1M~4M 수준입니다.

배치 처리 — 속도 제한 회피 토큰 버킷 구현

저는 토큰 버킷(Token Bucket) 알고리즘으로 동시 호출 수를 부드럽게 조절하는 AsyncRateLimiter 클래스를 만들었습니다. 비동기(async) 기반으로 설계해数千 건의 문서를 동시에 처리하면서도 분당 토큰 한도를 넘지 않습니다.

import asyncio
import time
from dataclasses import dataclass, field

@dataclass
class AsyncRateLimiter:
    rpm_limit: int            # 분당 최대 요청 수
    tpm_limit: int            # 분당 최대 토큰 수
    _request_times: list = field(default_factory=list)
    _token_history: list = field(default_factory=list)
    _lock: asyncio.Lock = field(default_factory=asyncio.Lock)

    async def acquire(self, estimated_tokens: int) -> None:
        async with self._lock:
            now = time.monotonic()
            window_start = now - 60

            # 60초 이전 기록 제거
            self._request_times = [t for t in self._request_times if t > window_start]
            self._token_history = [(t, tok) for t, tok in self._token_history if t > window_start]

            used_tokens = sum(tok for _, tok in self._token_history)
            sleep_for = 0.0

            if len(self._request_times) >= self.rpm_limit:
                sleep_for = max(sleep_for, self._request_times[0] + 60 - now)

            if used_tokens + estimated_tokens > self.tpm_limit:
                oldest = self._token_history[0][0]
                sleep_for = max(sleep_for, oldest + 60 - now)

            if sleep_for > 0:
                await asyncio.sleep(sleep_for)

            self._request_times.append(time.monotonic())
            self._token_history.append((time.monotonic(), estimated_tokens))

사용 예시

limiter = AsyncRateLimiter(rpm_limit=180, tpm_limit=2_000_000) async def safe_call(client, document, tokens_estimate): await limiter.acquire(tokens_estimate) return await asyncio.to_thread( client.chat.completions.create, model="gemini-2.5-pro", messages=[{"role": "user", "content": document}], max_tokens=1024, )

이 패턴으로 실행하니 1,000건 문서 묶음에서 429 응답이 0회로 떨어졌고, 평균 처리량은 분당 22.4건으로 안정화되었습니다.

재시도 전략 — 지수 백오프 + Jitter

분산 시스템에서 5xx 오류·네트워크 일시 단절·간헐적 429는 피할 수 없습니다. 단순히 except: pass로 처리하면 안 되고, 백오프에 무작위 지터(jitter)를 더해야 "thundering herd" 문제를 방지할 수 있습니다. 아래는 검증된 5단계 재시도 데코레이터입니다.

import random
import time
import functools
from openai import APIStatusError, RateLimitError, APITimeoutError

def retry_with_backoff(max_retries=5, base_delay=1.0, max_delay=32.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except RateLimitError as e:
                    # 429: Retry-After 헤더 우선 적용
                    wait = float(e.response.headers.get("retry-after", base_delay * (2 ** attempt)))
                    wait = min(wait, max_delay) + random.uniform(0, 0.5)
                    print(f"[{attempt+1}/{max_retries}] 429 → {wait:.2f}초 대기")
                    time.sleep(wait)
                except (APITimeoutError, APIStatusError) as e:
                    if attempt == max_retries - 1:
                        raise
                    wait = min(base_delay * (2 ** attempt), max_delay) + random.uniform(0, 1)
                    print(f"[{attempt+1}/{max_retries}] {type(e).__name__} → {wait:.2f}초 대기")
                    time.sleep(wait)
            raise RuntimeError("최대 재시도 횟수 초과")
        return wrapper
    return decorator

@retry_with_backoff(max_retries=5, base_delay=1.0)
def summarize_with_retry(client, text):
    return client.chat.completions.create(
        model="gemini-2.5-pro",
        messages=[{"role": "user", "content": f"요약:\n{text}"}],
        max_tokens=800,
    )

5회 재시도 + 지터를 적용한 결과, 1,000건 처리 시 최종 실패는 3건(0.3%)이었고, 모두 dead-letter 큐로 분리해 다음 사이클에서 재처리했습니다.

전체 파이프라인 조립 — 실전 코드

위에서 만든 두 컴포넌트를 합치면 진짜 운영 환경에서 돌아가는 코드가 됩니다.

import asyncio
import os
import json
from openai import AsyncOpenAI

client = AsyncOpenAI(
    api_key=os.environ["HOLYSHEEP_API_KEY"],
    base_url="https://api.holysheep.ai/v1",
)

async def process_document(doc_id: str, content: str, limiter, semaphore):
    # 토큰 추정: 한글은 보통 1.5자 = 1토큰
    estimated_tokens = len(content) // 2 + 512
    async with semaphore:
        await limiter.acquire(estimated_tokens)
        for attempt in range(5):
            try:
                resp = await client.chat.completions.create(
                    model="gemini-2.5-pro",
                    messages=[
                        {"role": "system", "content": "한국어로 5줄 요약"},
                        {"role": "user", "content": content},
                    ],
                    max_tokens=600,
                )
                return {"id": doc_id, "summary": resp.choices[0].message.content}
            except Exception as e:
                if attempt == 4:
                    return {"id": doc_id, "error": str(e)}
                await asyncio.sleep(min(2 ** attempt, 16) + 0.5)

async def main():
    documents = json.load(open("documents.json", encoding="utf-8"))
    limiter = AsyncRateLimiter(rpm_limit=180, tpm_limit=2_000_000)
    semaphore = asyncio.Semaphore(15)  # 동시 최대 15개

    tasks = [process_document(d["id"], d["body"], limiter, semaphore) for d in documents]
    results = await asyncio.gather(*tasks)

    with open("summaries.json", "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    asyncio.run(main())

이 파이프라인으로 5,000건 처리 시 실측 소요 시간은 약 3시간 47분, 총 비용 $362.50, 평균 지연 2,730ms가 나왔습니다.

자주 발생하는 오류와 해결책

오류 1. 429 RESOURCE_EXHAUSTED — 분당 토큰 초과

원인: 동일 분 안에 너무 많은 긴 문서를 몰아서 호출. 해결: 위의 AsyncRateLimiter로 TPM을 명시적으로 추적하거나, max_tokens를 1024 이하로 낮춰 출력 토큰 사용량을 통제합니다.

# 해결: 출력 토큰 상한 + 동시성 제한
semaphore = asyncio.Semaphore(10)        # 동시 호출 10개로 제한
resp = await client.chat.completions.create(
    model="gemini-2.5-pro",
    messages=[{"role": "user", "content": doc}],
    max_tokens=512,                       # 출력 토큰 상한 명시
)

오류 2. 400 INVALID_ARGUMENT — 컨텍스트 길이 초과

원인: 1M 토큰을 넘는 문서를 그대로 전송. 해결: 입력 단위로 청크 분할 후 부분 요약 → 통합 요약의 맵-리듀스 패턴을 사용합니다.

def chunk_text(text: str, chunk_size: int = 80_000) -> list[str]:
    return [text[i:i + chunk_size] for i in range(0, len(text), chunk_size)]

async def map_reduce_summary(client, text: str) -> str:
    chunks = chunk_text(text)
    partials = []
    for c in chunks:
        r = await client.chat.completions.create(
            model="gemini-2.5-flash",       # 부분 요약은 저가 모델로
            messages=[{"role": "user", "content": f"부분 요약:\n{c}"}],
            max_tokens=400,
        )
        partials.append(r.choices[0].message.content)
    final = await client.chat.completions.create(
        model="gemini-2.5-pro",            # 통합 요약은 상위 모델로
        messages=[{"role": "user", "content": "통합 요약:\n" + "\n".join(partials)}],
        max_tokens=1000,
    )
    return final.choices[0].message.content

오류 3. 500 INTERNAL — 간헐적 서버 오류

원인: Google 측 일시 장애 또는 네트워크 단절. 해결: 재시도 데코레이터를 적용하고, 실패한 작업은 별도 큐에 저장합니다.

from collections import deque

dead_letter = deque()

async def safe_process(doc):
    try:
        return await process_document(doc["id"], doc["body"], limiter, semaphore)
    except Exception:
        dead_letter.append(doc)        # 다음 사이클에서 재처리
        return None

후처리: dead_letter 재시도

async def replay_dead_letters(): while dead_letter: doc = dead_letter.popleft() await safe_process(doc)

오류 4. 한국어 인코딩 깨짐

원인: 파일을 binary 모드로 열거나, 시스템 기본 인코딩이 UTF-8이 아닐 때 발생. 해결: 명시적 encoding="utf-8" 지정 및 JSON 저장 시 ensure_ascii=False 옵션 사용.

with open("summaries.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

오류 5. 비용 폭증 — 무한 루프 재시도

원인: 4xx 오류(예: 잘못된 API 키)에도 재시도가 계속되어 비용만 누적. 해결: 재시도 대상을 429·5xx·타임아웃으로 한정하고 4xx는 즉시 실패 처리합니다.

except RateLimitError:           # 429만 재시도
    ...
except (APITimeoutError, APIStatusError) as e:
    if 400 <= e.status_code < 500:    # 4xx는 즉시 중단
        raise
    ...

운영 팁 — 한 단계 더

긴 문서 요약은 단순히 "큰 컨텍스트 모델을 호출한다"가 끝이 아닙니다. 속도 제한, 재시도, 비용 추적, 실패 격리를 하나의 파이프라인으로 엮어야 운영 환경에서 안정적으로 동작합니다. 저는 이 구성으로 arXiv 5,000건 파이프라인을 안정적으로 운영 중이며, 한 달 평균 $360 수준의 비용으로 동일한 품질을 유지하고 있습니다. 같은 워크로드를 Claude Sonnet 4.5로 돌렸다면 약 $825가 들었을 테니, 모델 선택만으로 월 $465(약 56%)를 절감한 셈입니다.

지금 막 긴 문서 자동화 파이프라인을 구축하기 시작했다면, 단일 API 키로 모든 모델을 돌릴 수 있는 게이트웨이가 진입 장벽을 크게 낮춰줍니다. HolySheep AI에 가입하면 가입 즉시 무료 크레딧이 제공되어, 별도 해외 신용카드 없이 오늘 바로 Gemini 2.5 Pro + 지수 백오프 재시도 파이프라인을 검증해볼 수 있습니다.

👉 HolySheep AI 가입하고 무료 크레딧 받기