에이전트 상태 저장·복구 — 중단을 전제로 설계하는 자동화
지난 3월, 블로그 발행 파이프라인이 새벽 2시에 죽었습니다. 로그를 보니 HTTP 429였어요. rate limit에 걸린 건데, 문제는 그 시점까지 작성한 초안 3편이 전부 사라졌다는 거였죠. 재시작하니까 처음부터 다시 수집을 시작하더라고요. 저는 에이전트가 죽는 것은 예외가 아니라 기본값이라고 생각합니다. 체크포인트 없는 자동화는 언젠가 반드시 작업을 잃거든요.
에이전트는 왜 중간에 죽나요?
실제로 돌려보면 API가 끊기는 경우의 수가 생각보다 많습니다. Anthropic 공식 문서 기준으로 HTTP 429는 rate limit 초과, 500은 내부 오류, 504는 타임아웃, 529는 과부하 상태예요. 제가 6개월간 운영하면서 본 건 주로 세 가지였습니다.
첫 번째는 rate limit이었어요. 하루 예산을 다 쓰거나 분당 요청 수가 초과되면 429 에러가 돌아옵니다. SDK가 자동으로 재시도를 두 번 하고 retry-after 헤더를 존중하지만, 그 안에서 회복되지 않으면 호출은 실패로 떨어집니다. 두 번째는 네트워크 타임아웃이었습니다. max_tokens를 크게 잡으면 응답이 늦어지는데, 일부 네트워크는 유휴 연결을 멋대로 끊어버려요. 세 번째는 529 과부하 에러였어요. 이건 Anthropic 서버가 과부하일 때 나오는데, 예측이 불가능합니다.
여기서 중요한 건, 이런 실패가 확률적으로 반드시 온다는 겁니다. 제 파이프라인은 하루에 LLM 호출을 평균 47번 해요. 각 호출당 실패 확률이 1%라고 쳐도, 한 달이면 최소 두세 번은 죽는다는 뜻이죠.
체크포인트 없이 자동화하면 어떻게 되나요?
상태 저장 없이 돌리면 중단 시점의 작업을 통째로 잃습니다. 제가 처음 만들었을 때가 그랬어요. 주제 수집 → 작가 배정 → 초안 생성 → 검증을 순차 실행했는데, 초안 생성 중간에 죽으면 그 전 단계까지 다시 돌아야 했거든요.
구체적으로 말하면, 주제 3개를 수집하고 초안 2개를 쓴 상태에서 세 번째 초안 작성 중 429가 나면, 재시작 시 다시 주제 수집부터 시작했습니다. 이미 쓴 초안 2개는 보존도 안 됐어요. 왜냐면 "전부 성공"했을 때만 파일에 쓰도록 했거든요.
이걸 해결한 방법은 단계마다 상태를 파일로 남기는 겁니다. 주제 수집이 끝나면 topics.json에 쓰고, 초안 하나 쓸 때마다 drafts/ 폴더에 저장하고, 검증 통과하면 published-index.json에 추가하는 식이에요. 지금은 어느 단계에서 죽든 그 직전까지의 작업은 보존됩니다.
멱등성이 왜 중요한가요?
재개할 때 같은 입력으로 여러 번 실행해도 결과가 달라지면 안 됩니다. 이게 멱등성이에요. 제가 겪은 사례는 중복 발행이었습니다.
초기에는 발행 스크립트가 "published 폴더에 파일이 있으면 스킵"하는 단순한 로직이었어요. 그런데 네트워크 타임아웃으로 파일 쓰기가 실패하면, 다음 실행 때 같은 글을 또 발행하려고 했죠. 실제로 한 번은 같은 초안이 slug만 다르게 두 번 올라간 적도 있습니다.
이걸 막으려면 발행 전에 "이미 발행됐는지"를 확인하는 게 아니라, 발행 후 상태를 먼저 기록해야 합니다. 저는 published-index.json에 draft_id를 append하고, 다음 발행 시엔 이 인덱스를 먼저 읽어서 중복을 걸러냅니다. 파일 쓰기가 실패해도 인덱스에 기록된 ID는 재발행을 막아줘요.
부분 실패도 고려해야 합니다. 검증자 5명이 병렬 실행되는데, 3명은 통과하고 2명만 실패하면 어떻게 하죠? 저는 각 검증자가 results/ 폴더에 자기 판정을 파일로 남기게 했습니다. 재실행 시 이미 통과한 검증자는 스킵하고, 실패한 것만 다시 돌려요.
실제로 얼마나 자주 복구가 일어나나요?
제 파이프라인은 하루 한 번 돌고, 지난 4개월간 총 17번 중단됐습니다. 평균 주 1회꼴이에요. 원인별로 보면 429가 9건, 504 타임아웃이 5건, 529 과부하가 3건이었습니다.
체크포인트를 도입하고 나서 복구 시간은 평균 2분 30초로 줄었어요. 예전엔 전체를 다시 돌려서 20분 넘게 걸렸거든요. 중단 지점부터 재개하니까 손실도 거의 없습니다.
관측 가능성도 중요합니다. 상태 파일에 타임스탬프와 attempt 번호를 남기면, 몇 번 재시도했는지 추적할 수 있어요. 저는 attempt가 3 이상이면 Telegram 알림을 보냅니다. 사람이 개입할 만한 문제라는 신호거든요.
성공 경로가 아니라 중단 지점부터 설계하라
자동화를 만들 때 "모든 게 잘 되면 어떻게 흐를까?"부터 생각하게 됩니다. 근데 실전에서 더 중요한 건 "어디서 죽을 수 있고, 그때 뭘 잃는가?"예요. 저는 이제 새 스크립트를 만들 때 체크포인트 위치부터 정합니다.
첫 번째 원칙은 상태를 외부에 쓰는 겁니다. 메모리에만 있는 건 죽으면 끝이에요. 파일이든 DB든 디스크에 기록해야 합니다. 두 번째는 재개 가능한 단위로 쪼개는 거예요. 한 번에 100건을 처리하지 말고, 10건씩 10번 나눠서 중간중간 저장하는 식이죠.
세 번째는 멱등성을 보장하는 겁니다. 같은 입력으로 여러 번 실행해도 결과가 똑같아야 해요. 네 번째는 부분 실패를 허용하는 구조입니다. 10개 중 7개 성공했으면 그 7개는 보존하고 나머지 3개만 재시도하는 거죠.
에이전트는 언젠가 반드시 죽습니다. 그게 언제인지 모를 뿐이에요. 중단을 전제로 설계하면 복구는 자동이고, 손실은 없습니다.
자주 묻는 질문
Q. 체크포인트를 너무 자주 만들면 성능이 느려지지 않나요?
A. 파일 쓰기 오버헤드는 실측해보니 호출당 5ms 정도였습니다. LLM 응답 대기가 평균 3초인 걸 감안하면 무시할 수준이에요. 단, SSD가 아니라 네트워크 스토리지를 쓴다면 병목이 될 수 있습니다.
Q. 상태 파일이 손상되면 어떻게 하나요?
A. JSON 파싱 실패 시 백업 파일로 폴백하게 했습니다. 매번 저장 전에 기존 파일을 .bak로 복사해두고, 새 파일 쓰기가 성공하면 백업을 지우는 식이에요.
Q. 멱등성을 보장하기 어려운 외부 API 호출은 어떻게 하나요?
A. 요청 전에 고유 ID를 생성해서 "이미 이 ID로 호출했는지" 로그에 기록합니다. 재시도 시 같은 ID면 스킵하거든요. 외부 API가 멱등키를 지원하면 그걸 쓰면 되고요.
Q. 에이전트가 중간에 죽었는데 어디서 재개해야 할지 모르겠어요.
A. 상태 파일에 last_completed_step 필드를 남기세요. 재시작 시 이걸 읽어서 다음 단계로 넘어가면 됩니다. 제 경우 run.json에 phase: "draft" 같은 값을 기록해둡니다.