자율 코딩 에이전트가 API 속도 제한 및 장애 상황에서 작동하게 만든 방법
요약
자율 코딩 에이전트 시스템의 안정성을 높이기 위해 API 속도 제한 및 일시적 장애 상황에 대응하는 네 가지 핵심 전략을 제시합니다. 오류 분류기, 지터 백오프를 사용한 재시도 로직, 공유 서킷 브레이커, 그리고 체크포인트/파킹 단계를 도입하여 시스템의 견고성을 확보했습니다.
핵심 포인트
- 오류 유형별로 명확히 분류하여 불필요한 재시도를 방지해야 합니다.
- 동시다발적 재시도(thundering herd)를 막기 위해 지터 백오프와 서킷 브레이커가 필수입니다.
- 작업 실패 시 처음부터 시작하는 대신, 체크포인트 및 파킹으로 진행 상황을 보존해야 합니다.
- API 속도 제한은 계정 단위로 공유되므로 시스템 전체의 안정성을 고려해야 합니다.
요약 (TL;DR)
제가 완전히 자율적으로 구현한 시스템은 24시간 연중무휴로 가동되는데, 몇 달 동안 가장 큰 문제가 되었던 것은 나쁜 코드가 아니라 새벽 3시에 모델 API가 "속도를 늦추라"거나 "과부하 상태다"라고 말하는 것이었습니다. 저는 네 가지 지루한 조각들로 이를 해결했습니다: 오류 분류기(error classifier), 재시도 예산(retry budget)을 사용한 지터 백오프(jittered backoff)와 재시도 로직, 모든 병렬 에이전트가 공유하는 서킷 브레이커(circuit breaker), 그리고 충돌 대신 사용하는 "체크포인트 및 파킹(checkpoint and park)" 단계입니다. 여기 빌드 로그와 과거의 저에게 해주고 싶은 다섯 가지 교훈을 소개합니다.
문제점 (The Problem)
제 시스템은 작은 규모로 구성되어 있습니다: 여러 병렬 구현 에이전트에게 작업을 할당하는 오케스트레이터 모듈과, 고장 난 작업을 처리하는 자가 복구(self-healing) 에이전트로 이루어져 있습니다. 모든 것은 호스팅된 LLM API와 통신합니다. 잘 작동할 때는 통합된 브랜치와 녹색 CI를 보며 잠에서 깨곤 합니다.
작동하지 않을 때는 다음과 같은 상황을 마주하게 됩니다:
[02:14:07] agent-3 task=refactor-auth ERROR 429 rate_limit_error
[02:14:07] agent-1 task=add-pagination ERROR 429 rate_limit_error
[02:14:08] agent-2 task=fix-flaky-test ERROR 429 rate_limit_error
...
동시에 세 가지 문제가 발생하고 있었습니다:
- 모든 오류가 똑같이 보였습니다. 429(속도 제한), 529/503(제공사 과부하), 400(프롬프트가 너무 큼) 등 모든 종류의 네트워크 리셋이 하나의
except Exception: retry()경로를 거쳤습니다. - 에이전트들이 동시에 재시도했습니다. 네 개의 에이전트가 한꺼번에 제한에 걸렸고, 동일한 고정 시간 동안 백오프(backoff)했다가 다시 한꺼번에 제한에 걸렸습니다. 전형적인 스런더링 허드(thundering herd) 현상이었습니다.
- "포기하는 것"은 작업 손실을 의미했습니다. 리팩터 작업을 40분 진행하다가 실패로 표시되면, 다음 실행은 처음부터 시작해야 했고 — 더 많은 토큰과 속도 제한을 소모하게 만들었습니다.
대략 2주에 걸쳐 계산해 보니: **실패한 작업의 31%**는 실제 실패가 아니었습니다. 그것들은 시스템이 영구적인 문제로 만들어버린 일시적인 API 오류들이었습니다.
흥미로운 제약 조건은 다음과 같았습니다: 저는 단순히 "할당량을 더 추가"할 수 없었습니다. 제한은 계정별로 적용되며, 모든 에이전트가 이를 공유하고, 장애는 제가 원하든 원하지 않든 발생합니다. 시스템은 단순히 재시도를 더 강하게 하는 것이 아니라 우아하게 성능을 저하시켜야 했습니다.
최종 설계의 형태는 다음과 같습니다:
flowchart LR
A[에이전트 호출] --> B{회로 개방?}
B -- 예 --> P[체크포인트 및 작업 주차]
...
아래 코드는 모두 Python 3.13이며, 핵심 기능만 간추렸습니다.
1. 재시도 전에 분류하기 (Classify before you retry)
가장 가치가 높았던 변경 사항은 오류를 하나의 범주로 취급하는 것을 거부한 것입니다.
from enum import Enum
class ErrKind(Enum):
...
이 FATAL 범주는 보이는 것보다 더 중요합니다. 이전에는 컨텍스트 창을 초과하는 프롬프트가 5번 재시도되었는데, 이는 동일하고 보장된 실패를 의미했으며, 매번 다른 에이전트들이 필요로 하는 속도 제한(rate limit)을 소모했습니다. 이제 400 오류는 명확한 이유와 함께 즉시 실패하며, 자가 복구 에이전트는 이를 활용하여 실제로 유용한 작업(예: 작업을 분할하는 것)을 수행할 수 있습니다.
2. 재시도 횟수가 아닌 재시도 예산으로 지터링 백오프 적용하기 (Jittered backoff with a retry budget, not a retry count)
고정된 재시도 횟수(
import json, time
from pathlib import Path
from filelock import FileLock
...
브레이커가 열리면 아무도 5분 동안 API를 호출하지 않습니다. 냉각 기간(cooldown)이 지나면 깨어난 첫 번째 에이전트가 프로브 역할을 합니다. 성공하면 카운터가 초기화되고 다른 모든 에이전트가 진행합니다. 실패하면 브레이커가 다시 열립니다. 이 방법으로 제 '에러 스톰' 로그는 수백 줄에서 몇 줄로 줄었습니다.
4. 충돌(crash) 대신 체크포인트 저장 및 대기(park)하기
이것이 실제로 작업 시간을 절약해 준 부분입니다. 태스크가 재시도 예산이 소진되거나 열린 브레이커에 도달해도 실패하지 않습니다. 대신 체크포인트를 작성하고 PARKED 상태로 들어갑니다:
def park(task, reason: str) -> None:
task.checkpoint = {
"branch": task.branch, # 작업은 이미 여기에 커밋됨
...
오케스트레이터는 PARKED 상태를 FAILED 상태와 다르게 취급합니다. 대기 중인 태스크는 브레이커가 닫히면 가장 먼저 재개되며, 프롬프트에 주입된 메모(notes)와 함께 next_step부터 시작합니다. 각 에이전트가 의미 있는 단계마다 자신의 브랜치에 커밋하기 때문에, '재개'는 저렴합니다. 에이전트는 전체 히스토리를 읽는 것이 아니라 체크포인트와 디프(diff)만 읽기 때문입니다.
현재의 상황은 다음과 같습니다:
[02:14:07] agent-3 429 rate_limit -> 3.1초 대기 (예산 476초 남음)
[02:14:08] agent-1 429 rate_limit -> 0.7초 대기 (예산 480초 남음)
[02:14:11] agent-2 529 overloaded -> 14.2초 대기
...
수치로 본 변화
이것을 약 6주 동안 실행한 결과:
- ✅ 일시적인 API 오류로 실패하는 태스크: 전체 실패 건수의 31% → 3% 미만으로 감소
- ✅ 치명적 오류(크기가 큰 프롬프트 등)로 인한 낭비된 재시도: 사실상 제로
- ✅ 대기 상태인 태스크당 평균 추가 벽시계 시간: 이전의 전체 재시작 대비 약 7분으로 단축
- ⚠️ 아침에 마주치는 예상치 못한 문제: 여전히 0은 아니지만, 이제는 제가 원하는 실제 버그들입니다.
배운 교훈
배운 교훈
1. 대부분의 '에이전트 실패'는 인프라 문제라는 코스튬을 입고 나타납니다. 💡 모델의 추론 능력을 비난하기 전에, 몇 번의 실패가 네트워크나 제공업체(provider) 때문이었는지 확인하세요. 저에게는 그 비율이 거의 3분의 1에 달했습니다. 배관 공사를 고치는 것이 그 달에 제가 시도했던 어떤 프롬프트 수정보다 효과적이었습니다.
2. 분류하지 않은 오류는 절대 재시도하지 마세요. 무차별적인 재시도는 저렴하고 즉각적인 실패(잘못된 요청, bad request)를 느리고 비용이 많이 드는 문제로 만들고 — 건강한 작업에서 용량을 빼앗아 갑니다. 이 글에서 단 한 가지만 실천한다면, 분류기(classifier)를 만드세요.
3. 시도 횟수가 아닌 시간을 예산으로 책정하세요. '5회 재시도'라는 것은 백오프(backoff) 방식에 따라 매우 다르게 해석됩니다. 작업당 대기 시간 예산을 설정하면, 논리적으로 이해할 수 있는 엄격한 상한선이 생기고 장기 작업과 단기 작업이 공평하게 작동하도록 만듭니다.
4. 병렬 에이전트는 공유된 실패 상태가 필요합니다. 각 에이전트가 개별적으로 예의 바른 것만으로는 충분하지 않습니다. 이들이 브레이커(breaker)를 공유하지 않으면, 어려움을 겪는 API를 집단적으로 과부하 시킬 것입니다. 공유 상태는 놀라울 정도로 간단할 수 있습니다 — 몇몇 에이전트에게는 잠긴 JSON 파일만으로도 잘 작동했습니다.
5. 성공이나 실패뿐 아니라 '일시 정지(pause)'에 대비하여 설계하세요. PARKED 상태가 진정한 해답을 제공했습니다. 장기 실행되는 자율 작업에는 세 번째 결과가 필요합니다: '안전하게 멈췄고, 어디서 다시 시작해야 할지 정확히 알고 있다.' 이것은 에이전트가 자주 커밋하고 진행하면서 짧은 메모를 남길 때만 작동합니다.
앞으로의 계획 (What's Next)
현재 작업 중인 몇 가지 사항들이 있습니다:
- 우선순위 기반 스로틀링(Priority-aware throttling) — 할당량(quota)이 빠듯할 때, 짧고 가치 높은 작업을 먼저 진행하고 대규모 리팩토링은 보류합니다.
- 저위험 단계용 폴백 모델(Fallback models for low-risk steps) — diff 요약이나 커밋 메시지 작성 같은 작업에는 가장 강력한 모델이 필요하지 않으므로, 동일한 한도(limit)를 두고 경쟁해서는 안 됩니다.
- 원격 제어 대시보드에 브레이커 작동 알림 표시하여 조용한 밤이 '할 일이 없었는지' 아니면 '제공업체가 다운되었는지' 한눈에 볼 수 있게 합니다.
마무리 (Wrap-up)
만약 에이전트를 무인 상태로 실행한다면 — 심지어 Claude Code 세션을 루프(loop)로 한 번 돌리는 것만으로도 — 신뢰성의 상한선은 프롬프트가 아니라 오류 처리 방식일 가능성이 높습니다. 분류하고, 지터(jitter)와 함께 백오프(back off)하며, 브레이커(breaker)를 공유하고, 충돌하는 대신 파킹(park)하세요.
👉 Dev.to에서 저를 팔로우하여 완전히 자율적인 코딩 시스템을 실행한 빌드 로그를 더 받아보시고, 댓글로 알려주세요: API 장애가 여러분의 에이전트 설정을 망가뜨린 가장 이상했던 방법은 무엇인가요? 전쟁 이야기를 비교해 보고 싶습니다. 🚀
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기