에이전트 호출 쓰기 작업을 멱등하게 만드세요, 그렇지 않으면 데이터를 잃게 됩니다
요약
에이전트 네이티브 제품 개발 시 발생할 수 있는 중복 쓰기 문제를 방지하기 위한 멱등성(Idempotency) 설계의 중요성을 다룹니다. 에이전트의 자동 재시도와 시간 초과로 인한 중복 호출 상황에서 데이터를 안전하게 보호하는 방법을 설명합니다.
핵심 포인트
- 에이전트의 자동 재시도는 '최소 한 번 전달' 문제를 일상화함
- 의도 발생 시 고유 키를 생성하고 재시도 시 동일 키를 재사용해야 함
- 서버는 키를 통해 작업을 수행하거나 기존 결과를 반환해야 함
- 경합 조건을 방지하기 위해 원자적 연산(Atomic operation) 사용 필수
에이전트 네이티브 (agent-native) 제품은 CRUD 앱에 챗봇을 덧붙인 것이 아닙니다. 그것은 에이전트가 무언가를 수행할 수 있게 해주는 MCP 서버입니다. 송장을 읽고, 비용을 생성하며, 고객의 미납 상태를 표시합니다. 데모는 쉽습니다. 하지만 실제 트래픽과 접촉했을 때 제품이 생존할지를 결정하는 부분은 아무도 스크린샷을 찍지 않는 계층, 즉 쓰기 작업이 두 번 전송될 때 어떤 일이 발생하는가 하는 점입니다.
이것은 에이전트 네이티브 엔지니어링의 화려하지 않은 절반입니다. 에이전트가 호출할 수 있는 모든 쓰기 작업 (write operation)은 멱등성 (idempotency), 안전한 재시도 (safe retries), 그리고 타입화된 복구 가능한 오류 (typed recoverable errors)를 갖춰야 합니다. 이는 있으면 좋은 기능이 아니라, 제품과 부채를 가르는 차이입니다. 그 이유와 구축 방법은 다음과 같습니다.
에이전트는 "최소 한 번 (at-least-once)" 전달을 각주에서 일상적인 사건으로 바꿉니다
분산 시스템 (Distributed systems) 전문가들은 네트워크가 정확히 한 번 (exactly-once)이 아닌 최소 한 번 (at-least-once) 전달을 보장한다는 사실을 아주 오래전부터 알고 있었습니다. 요청이 나가고, 응답이 돌아오는 길에 유실되면, 호출자는 쓰기 작업이 성공했는지 알 수 없으므로 재시도합니다. 사람이 버튼을 클릭하는 경우, 이는 대충 넘어가도 될 정도로 드문 일입니다.
에이전트는 이러한 대충 넘어가기를 불가능하게 만듭니다. 에이전트가 도구 (tools)를 호출하는 방식에 관한 세 가지 요소는 중복 쓰기 문제를 "예외적인 상황"에서 "매주 화요일에 일어나는 일"로 격상시킵니다:
- 자동 재시도합니다. 잘 작동하는 MCP 클라이언트는
429와5xx오류에 대해 백오프(backoff)하며 재시도합니다. Frihet MCP 서버 (github.com/Frihet-io/frihet-mcp)는fri_키당 분당 100 요청을 강제하며, 자체 클라이언트는 이 한계에 도달하면 지수 백오프(exponential backoff)를 사용하여 재시도합니다.429이후의 재시도는 발생해야 하는 것으로 간주되므로 — 이는 해당 엔드포인트 뒤에서 발생하는 모든 쓰기 작업이 한 번 이상 전달되는 것을 견뎌야 함을 의미합니다. - 시간 초과 및 재개됩니다. 에이전트 루프가 도구 호출 중간에 실시간 시간 제한(wall-clock limit)에 부딪히면, 세션은 재개되고 플래너는 — 결과를 본 적이 없으므로 — 합리적으로
클라이언트의 규칙은 간단합니다: 의도(intent)가 발생하는 시점에 키를 한 번 생성하고, 동일한 의도에 대한 모든 재시도(retry)에서 해당 키를 재사용하십시오. 만약 에이전트가 클라이언트에게 비용을 청구하기로 결정했다면, 하나의 키를 발행(mint)하고 네 번의 모든 전달 시도 과정 동안 이를 유지합니다. 다른 클라이언트에게 비용을 청구하기로 결정했을 때는 새로운 키를 발행합니다.
서버의 역할은 그 키에 의미를 부여하는 것입니다: 키를 처음 보았을 때는 작업을 수행하고 결과를 기억합니다; 그 이후의 모든 경우에는 데이터베이스를 다시 건드리지 않고 기억된 결과를 반환합니다.
사람들을 곤란하게 만드는 미묘한 차이는 원자성 (atomicity)입니다. "키가 존재하는지 확인한 다음, 삽입한다"는 방식은 경합 조건 (race condition)을 유발합니다. 즉, 두 개의 동시 재시도가 모두 "찾을 수 없음"을 읽고 둘 다 작업을 수행하게 됩니다. 이 작업은 단일 원자적 연산 (single atomic operation)이어야 하며, Postgres에서는 INSERT ... ON CONFLICT DO NOTHING이고, 대부분의 KV 저장소에서는 compare-and-set 방식입니다.
import { createHash } from "node:crypto";
interface WriteContext {
...
이 코드 스니펫이 단순한 버전들이 놓치는 세 가지를 제대로 구현한 점은 다음과 같습니다:
- 키는 테넌트(tenant)별로 범위가 지정됩니다. 두 계정이 동일한 UUID를 생성할 수 있습니다; 슬롯에
accountId가 없다면, 한 고객의 재시도가 다른 고객의 인보이스(invoice)를 반환할 수 있습니다. 공격적으로 범위를 지정하십시오. - 페이로드(payload)에 지문(fingerprint)이 찍힙니다. 다른 본문과 함께 키를 재사용하는 것은 거의 항상 클라이언트의 버그입니다. 이전 결과를 조용히 반환하는 것은 버그를 숨기는 것이고, 새 레코드를 생성하는 것은 목적에 어긋납니다. 타입이 지정된 에러(typed error)로 거부하는 것이 정직한 답변입니다.
- 저장된 결과가 있는 그대로 반환됩니다. 전체 계약의 핵심은 두 번째 호출자가 첫 번째 호출자가 받은 것과 정확히 동일한 것(동일한 인보이스 ID, 동일한 숫자)을 받는다는 것입니다. 단순히 비슷해 보이는 새로운 쓰기가 아닙니다.
기록을 얼마나 오래 유지해야 할까요? 어떤 재시도 폭풍(retry storm)보다 더 오래 살아남을 수 있을 만큼 충분히 길어야 합니다. 보통 몇 시간에서 하루 정도가 일반적이며, 이는 기본값이 아닌 의도적인 트레이드오프 (trade-off)입니다. 너무 짧으면 느린 재시도가 윈도우를 빠져나가 중복 쓰기가 발생하고, 너무 길면 모든 요청 본문을 영원히 저장하게 됩니다.
안전한 재시도에는 try/catch가 아닌 분류 체계가 필요합니다
멱등성 (Idempotency)은 재시도를 안전하게 만듭니다. 하지만 멱등성이 언제 재시도해야 하는지를 알려주지는 않습니다. 그 결정은 무엇이 잘못되었는지에 전적으로 달려 있으며, "에러가 발생했습니다"라는 정보만으로는 결정을 내리기에 충분하지 않습니다.
429또는503은 일시적인 (transient) 오류입니다. 잠시 대기한 후 재시도하십시오.422 validation_failed는 영구적인 (permanent) 오류입니다. 잘못된 페이로드 (payload)를 수천 번 재시도하는 것은 단순히 속도 제한 (rate limit)만 낭비하는 꼴입니다.504게이트웨이 타임아웃 (gateway timeout)은 정말 까다로운 문제입니다. 쓰기 작업이 성공했을 수도 있고, 실패했을 수도 있기 때문입니다. 이것이 바로 멱등성이 만들어진 정확한 사례입니다. 동일한 키를 사용하여 재시도하면, 서버는 원래 작업을 완료하거나 이미 생성된 결과를 반환합니다. 키가 없다면, 여기서의 재시도는 누군가에게 이중 결제를 유발하는 원인이 됩니다.
에이전트 (agent)가 이 호출을 수행하려면, 에러는 기계가 읽을 수 있는 (machine-readable) 형태여야 합니다. 문자열로 된 500 Internal Server Error는 모델이 산문(prose)을 통해 추측하게 만들며, 모델은 추측에 실패하곤 합니다. 대신 타입화된 에러 분류 체계 (typed error taxonomy)를 배포하십시오. 즉, 안정적인 code, 명시적인 retryable 불리언 (boolean), 그리고 얼마나 기다려야 하는지에 대한 힌트를 제공해야 합니다.
type ErrorCode =
| "rate_limited" // 429 — 대기 후 재시도
| "upstream_timeout" // 504 — 동일한 멱등성 키 (idempotency key)로 재시도
...
이 분류 체계를 지수 백오프 (exponential backoff) _및 지터 (jitter)_와 결합하십시오. 지터가 없는 백오프는 속도 제한에 걸린 모든 도구 호출 (tool calls)이 동일한 순간에 재시도하게 만듭니다. 이는 다시 제한을 건드리는 동기화된 돌진 (synchronized stampede)을 초래합니다. 지연 시간을 무작위화하면 무리를 분산시킬 수 있습니다. 또한 시도 횟수에 상한을 두십시오. 재시도 가능 (retryable)하다고 해서 영원히 재시도하라는 뜻은 아닙니다.
retryable은 계약 (contract)의 서버 측에 존재하지만, 클라이언트가 이를 _소비 (consumed)_한다는 점에 주목하십시오. 그것이 핵심입니다. 서버는 쓰기 작업을 반복하는 것이 안전한지 알고 있으므로, 모든 호출자가 HTTP 상태 코드와 희망 사항을 바탕으로 답을 역공학 (reverse-engineer)하게 만드는 대신 명시적으로 알려주는 것입니다.
이것이 에이전트 네이티브 (agent-native) 제품에서 전부인 이유
데모는 에이전트가 송장(invoice)을 생성할 수 있다는 것을 증명합니다. 하지만 프로덕션(Production) 환경은 더 어려운 질문을 던집니다. 네트워크 오류가 발생하거나, 세션이 재개되거나, 클라이언트가 수행하도록 설계된 429 오류로 인한 재시도(retry)가 발생하여 동일한 명령이 두 번 전달될 때, 고객에게 생성되는 송장은 하나입니까, 아니면 두 개입니까?
영향 범위(surface area)를 고려해 보십시오. Frihet와 같은 서버는 mcp.frihet.io를 통해 157개의 도구(tools)를 노출하며, 그중 상당수는 실제 세무 기록을 생성하는 ERP에 데이터를 기록(write)합니다. 이러한 모든 쓰기 도구는 최소 한 번 전달(at-least-once delivery) 방식이 재무적 부작용(fiscal side effect)과 만나는 지점입니다. 멱등성 키(Idempotency keys), 서버 측의 원자적 중복 제거(atomic server-side dedup), 그리고 타입화된 복구 가능한 오류(typed recoverable errors)는 이러한 영역을 중복 데이터 생성기가 아닌, 에이전트가 분당 100개의 요청을 안전하게 몰아칠 수 있는 환경으로 바꿔줍니다. 이 글의 논거는 바로 그 계약(contract)에 있습니다. 그것은 에이전트 반대편에 있는 모든 서버가 호출자에게 마땅히 제공해야 할 의무입니다. 또한 이는 코드베이스에서 가장 화려하지 않은 작업이자, 제품의 신뢰성을 결정짓는 작업이기도 합니다.
업계는 결제(payments) 분야에서 이를 배우는 데 10년을 보냈습니다. Stripe가 멱등성 키(idempotency keys)를 퍼스트 클래스 헤더(first-class header)로 만든 이유이기도 합니다. 에이전트는 기본적으로 재시도(retry)하고, 재개(resume)하며, 팬아웃(fan out)하기 때문에, 에이전트는 이 교훈을 모든 곳에서 즉각적으로 시급하게 만듭니다. 에이전트가 기록할 수 있는 무엇인가를 구축하고 있다면, 이를 단순한 다듬기(polish)가 아닌 기초(foundation)로 취급하십시오. 챗봇은 사용자가 보는 부분입니다. 이것은 사용자의 데이터를 정확하게 유지하는 부분입니다.
다음 주제: 에이전트가 오용하지 않을 도구 스키마(tool schemas) 설계 — 실제 인터페이스로서의 타입(types), 열거형(enums), 그리고 오류 계약(error contracts).
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기