iBetter Books
수정

장애 사례와 해결 경로

프로덕션 장애는 대부분 사양을 몰라서가 아니라, 데모에서 보이지 않던 한계가 누적·반복·실패라는 운영 조건 아래에서 임계점을 넘기 때문에 생긴다. 사후 분석의 핵심은 에러 메시지가 아니라 그 뒤에 숨은 구조적 가정이다. 시험도 같은 시선으로 묻는다. 장애 로그를 보여 주고 "근본 원인은 무엇인가", "재발을 막는 설계는 무엇인가"를 선택하게 한다. 표면 증상만 고치는 보기는 거의 항상 오답이다. 이 절은 세 가지 대표 패턴을 증상에서 근본 원인을 거쳐 해결 경로로 따라간다.

패턴 1 — 컨텍스트 오버플로

증상은 장시간 세션이나 도구 결과가 큰 작업에서 어느 순간 응답이 잘리거나, 요청이 거부되거나, 출력이 중간에 끊기는 것이다. 근본 원인은 단순하다. Anthropic API는 stateless이므로, 누적된 대화 이력과 도구 결과를 매 요청 전부 다시 보낸다. 이 입력 토큰이 모델의 컨텍스트 윈도우를 향해 계속 차오르는데도 클라이언트가 줄여 주지 않으면 결국 한도를 넘는다. 여기서 모델 세대에 따라 동작이 갈린다는 점이 시험의 단골 함정이다. Claude 4.5 이상에서는 입력 토큰과 max_tokens의 합이 윈도우를 넘어도 요청을 받아들이고, 생성 중 윈도우 끝에 닿으면 stop_reason: "model_context_window_exceeded"로 멈춘다. 반면 이전 세대 모델은 같은 상황에서 검증 에러를 반환한다. 이전 모델에서 4.5식 동작을 쓰려면 model-context-window-exceeded-2025-08-26 베타 헤더로 옵트인해야 한다.

따라서 첫 번째 진단 도구는 토큰 카운팅 API다. 요청을 보내기 전에 입력 토큰을 세어, 입력과 max_tokens의 합이 윈도우 안에 들어오는지 확인한다. 예를 들어 입력이 19만 토큰인데 max_tokens가 2만이면 20만 윈도우를 넘으니, max_tokens를 1만으로 낮추면 합이 다시 한도 안으로 들어온다. 해결 경로는 두 갈래다. 단기적으로는 max_tokens를 윈도우에 맞춰 예산화하고, 근본적으로는 누적 이력을 통제한다. 시스템 프롬프트와 최근 N개 메시지는 그대로 두고 오래된 턴은 요약 블록으로 압축하는 슬라이딩 윈도우·요약 전략이 표준이다. 시험이 노리는 오답은 "max_tokens만 키우면 된다"는 보기다. max_tokens는 출력 상한일 뿐이라 입력 누적 문제를 풀지 못하고, 오히려 합을 키워 오버플로를 앞당긴다.

패턴 2 — 종료 조건을 잃은 루프

증상은 에이전트가 끝나지 않고 같은 도구를 반복 호출하거나, 비슷한 시도를 무한히 되풀이하며 세션이 멈추지 않는 것이다. 근본 원인은 종료의 책임 소재에 대한 오해다. 에이전트 루프의 정지는 모델이 보장하지 않는다. 모델은 매 턴 다음 행동을 제안할 뿐이고, 도구 호출을 그만둘지 결정하는 것은 그 루프를 도는 하네스다. 깨진 파일을 계속 읽으려 재시도하거나, 실패한 명령을 같은 방식으로 반복하면 모델은 매번 "다시 시도"를 제안하고 하네스가 그 제안을 그대로 실행하며 루프가 무한히 돈다. 시험은 이 책임 소재를 직접 묻는다. "모델이 알아서 멈춘다", "프롬프트로 '무한히 돌지 마'라고 지시하면 된다"는 보기는 모두 오답이다. 종료는 코드가 강제하는 불변식이지 모델의 협조에 기대는 부탁이 아니다.

해결 경로는 루프에 외부 경계를 거는 것이다. Claude Code 계열에서는 --max-turns--max-iterations 같은 가드레일로 세션이 돌 수 있는 턴·반복 횟수에 상한을 둔다. 이는 불가능한 작업이나 진전 없는 재시도가 무한정 자원을 태우는 것을 막는다. 다만 횟수 상한은 무딘 차단막이라, 잘 설계된 루프는 여기에 더해 명시적 정지 기준을 둔다. 작업 완료를 판정하는 검증 단계를 두고, 진전이 없으면 멈추며, 동일한 실패가 반복되면 다른 전략으로 전환하거나 사람에게 에스컬레이션한다. 22장의 서킷 브레이커가 외부 의존성의 반복 실패를 차단하듯, 루프 가드레일은 에이전트 자신의 반복 실패를 차단한다. 핵심은 "정상 종료"와 "강제 종료"를 모두 설계에 넣는 것이다. 정상 종료는 완료 판정이 책임지고, 강제 종료는 횟수 상한이 책임진다.

패턴 3 — 캐시 붕괴와 재시도 증폭으로 인한 비용 폭증

증상은 트래픽이 평소와 비슷한데 토큰 청구가 급등하거나, 지연이 늘면서 비용이 함께 치솟는 것이다. 비용 폭증은 보통 두 가지 원인이 겹친다. 첫째는 프롬프트 캐시 붕괴다. 프롬프트 캐시는 도구·시스템·메시지를 그 순서대로, cache_control로 지정한 블록까지의 접두사 전체를 참조한다. 그래서 접두사 앞쪽이 한 글자라도 바뀌면, 예컨대 시스템 프롬프트에 현재 시각이나 요청 ID를 끼워 넣으면 매 요청 캐시가 무효가 되어 매번 전체를 다시 읽는다. 캐시 읽기는 기본 입력 토큰보다 훨씬 싸지만, 캐시 쓰기는 기본 입력보다 비싸고 1시간 TTL 쓰기는 5분보다 더 비싸다. 캐시가 계속 깨지면 매번 비싼 쓰기 요금만 내고 싼 읽기 이득은 못 얻으니, 캐시를 안 쓰는 것보다 더 비싸질 수 있다. 시험은 이 역설을 노린다. "캐시를 켰는데 비용이 올랐다"는 사례의 근본 원인은 대개 접두사 앞단의 가변 데이터다.

둘째는 재시도 증폭이다. 429나 529를 받았을 때 재시도 자체는 옳지만, 응답의 retry-after 헤더를 무시하고 즉시·고정 간격으로 재시도하면 한도를 더 빠르게 다시 치며 실패한 요청을 증식시킨다. 게다가 패턴 2의 루프가 결합되면, 종료 조건을 잃은 에이전트가 실패한 도구 호출을 수천 번 반복하며 하루 예산을 순식간에 태운다. 해결 경로는 세 갈래다. 캐시 측에서는 가변 데이터를 접두사 뒤로 밀어 캐시 접두사를 안정화한다. 재시도 측에서는 retry-after를 준수하고 지수 백오프와 지터를 적용한다. 예산 측에서는 워크스페이스·기능 단위로 요청 속도 제한을 걸어, 폭주한 배치 작업이 대화형 트래픽을 굶기거나 예산을 단번에 소진하지 못하게 막는다. 모델 설정으로 비용을 줄이려는 보기, 예컨대 출력을 강제로 줄이는 옛 파라미터를 끼워 넣는 보기는 함정이다. 현재 모델에서는 출력 길이 미세 조정용 옛 샘플링 파라미터를 쓰지 않으며, thinking은 adaptive만 쓰고 구조화 출력은 output_config.format으로 다룬다. 비용 통제의 정답은 파라미터 튜닝이 아니라 캐시 안정화·재시도 규율·속도 제한이라는 구조적 장치다.

정리

  • 컨텍스트 오버플로의 근본 원인은 stateless API가 누적 이력을 매번 다시 보내는데 클라이언트가 줄이지 않는 것이다. Claude 4.5 이상은 stop_reason: "model_context_window_exceeded"로 멈추고 이전 모델은 검증 에러를 내며(model-context-window-exceeded-2025-08-26 베타로 옵트인). 토큰 카운팅으로 사전 진단하고 슬라이딩 윈도우·요약으로 흡수한다. max_tokens만 키우는 것은 오답이다.
  • 에이전트 루프의 종료는 모델이 아니라 하네스의 책임이다. "모델이 알아서 멈춘다"·"프롬프트로 지시하면 된다"는 오답이며, --max-turns·--max-iterations 같은 횟수 상한과 완료 판정·진전 검사·반복 실패 시 전환이라는 명시적 정지 기준을 함께 둔다.
  • 비용 폭증은 캐시 붕괴와 재시도 증폭이 겹친 결과다. 캐시는 cache_control까지의 접두사 전체를 참조하므로 앞단 가변 데이터(시각·요청 ID)가 매 요청 캐시를 무효화하고, 쓰기 요금만 내며 더 비싸진다. 가변 데이터를 뒤로 밀어 접두사를 안정화한다.
  • 재시도는 retry-after를 준수하고 지수 백오프·지터를 적용하며, 워크스페이스·기능 단위 속도 제한으로 폭주 작업을 격리한다. 비용 통제의 정답은 옛 샘플링 파라미터 튜닝이 아니라 캐시 안정화·재시도 규율·속도 제한이라는 구조적 장치다.