
AI 에이전트를 위한 PRG 패턴: 새로운 시대에 성숙해진 25년 된 해결책
요약
웹 개발의 고전적인 PRG(Post/Redirect/Get) 패턴이 AI 에이전트 환경에서 다시 중요해지고 있습니다. 에이전트 간 통신이나 파이프라인 구축 시 발생할 수 있는 중복 작업 문제를 방지하기 위한 해결책을 제시합니다.
핵심 포인트
- PRG 패턴은 POST 요청 후 리다이렉트를 통해 중복 실행을 방지함
- AI 에이전트 및 A2A, MCP 환경에서 동일한 중복 실행 버그가 재발함
- GET 요청의 멱등성을 활용하여 에이전트 작업의 안정성을 확보해야 함
- 작업 수행(POST)과 결과 표시(GET)를 분리하는 것이 핵심
90년대부터 웹 폼(web forms)에는 항상 고전적인 버그가 괴롭혀 왔습니다. 아마 여러분도 본 적이 있을 것입니다 — _"이 양식을 다시 제출하면 동작이 반복됩니다."_라는 브라우저 경고 말입니다. 사용자가 주문을 하고 새로고침을 누르면, 이제 주문이 두 건이 됩니다. 혹은 이메일이 두 통 가거나, 결제가 두 번 발생하기도 합니다.
두 건의 주문. 두 번의 결제. 한 명의 좌절한 고객.
해결책은 Post/Redirect/Get (PRG) 패턴이었습니다. 우아하고 단순하며, 대부분 잊혀진 방식입니다. 이 패턴은 서버 사이드 프레임워크(server-side frameworks)의 리다이렉트 헬퍼(redirect helpers)로 흡수되었기에, 새로운 개발자들은 이를 의식적으로 사용할 필요가 없었습니다. 그 후 클라이언트 사이드 JavaScript가 이 루프를 완전히 닫아버렸습니다 — XHR 콜백(callbacks), jQuery deferreds, 그리고 이후 등장한 모든 프레임워크의 async/await까지 말이죠. 상태 변경(Mutations)은 자기 완결적인 작업이 되었습니다. 스피너(Spinners), 낙관적 업데이트(optimistic updates), 로딩 인디케이터(loading indicators)는 "동작을 수행하는 것"과 "결과를 보여주는 것" 사이의 마지막 가시적인 경계를 제거했습니다. 다시 재생할 페이지가 없을 때는 POST 재전송(POST-replay)에 대해 생각하지 않게 됩니다.
이 패턴이 사라진 것은 문제가 해결되었기 때문이 아닙니다. 기술 스택이 이 패턴을 보이지 않게 만들었기 때문입니다.
AI 에이전트(AI agents)는 새로운 계층에서 바로 이 동일한 버그를 다시 도입했습니다. 그리고 A2A, MCP, 또는 커스텀 에이전트 파이프라인(agentic pipelines)을 기반으로 구축하는 대부분의 팀은 이제 처음으로 이 문제와 마주하게 될 것입니다.
PRG란 실제로 무엇인가
PRG가 없다면, 웹 폼은 다음과 같이 작동합니다:
- 사용자가 폼을 작성하고 Submit을 클릭합니다.
- 브라우저가 서버로 POST를 보냅니다.
- 서버가 카드를 결제하고, 이메일을 보내고, 레코드를 생성합니다.
- 서버가 성공 페이지로 응답합니다.
- 사용자가 Refresh를 누릅니다.
- 브라우저가 POST를 재전송합니다 — 모든 것이 다시 실행됩니다.
해결책은 간단합니다: POST에 대해 페이지로 응답하지 마십시오. 대신:
- 사용자가 제출함 → 브라우저가 POST를 전송
- 서버가 작업을 수행
- 서버가 결과 URL로 302 Redirect (리다이렉트) 응답을 보냄
- 브라우저가 이를 따름 → GET을 전송
- 서버가 결과 페이지를 반환
- 사용자가 **새로고침 (Refresh)**을 누름 → 브라우저가 GET을 재실행하며, 이는 무해함
위험한 POST는 이제 새로고침으로 도달할 수 없게 됩니다. 리다이렉트는 "작업을 수행하는 것"과 "작업의 결과를 보여주는 것" 사이의 관문 역할을 합니다.
이 방식이 작동하는 세 가지 요소가 있습니다. 첫째, POST는 정확히 한 번만 실행됩니다. 리다이렉트가 사용자를 즉시 POST 영역에서 벗어나게 하기 때문입니다. 둘째, 리다이렉트는 결과를 고정하는 안정적인 ID(주문 번호 또는 트랜잭션 ID)를 전달합니다. 셋째, GET은 멱등성 (Idempotent)을 가집니다. 즉, 백 번을 호출해도 동일한 페이지를 반환할 뿐 새로운 작업을 수행하지 않습니다.
HTTP/1.1에는 심지어 이를 위한 상태 코드인 303 See Other, 즉 "이 요청에 대한 응답을 GET을 사용하여 가져오십시오"라는 의미의 코드가 포함되어 있었습니다. 초기 브라우저들이 303에 대해 일관되지 않았기 때문에 대부분의 사람들이 302를 사용했지만, 그 의도는 1999년 사양(Spec)에 이미 명시되어 있었습니다. 이듬해 REST가 이론적 토대를 완성했습니다. GET은 안전하고 멱등하지만, POST는 둘 다 아닙니다. 이러한 어휘를 갖추고 나면 해결책은 명확해집니다. 사양, 상태 코드, 그리고 이론은 에이전트 프레임워크가 등장하기 훨씬 전부터 이미 존재했습니다.
에이전트도 정확히 똑같은 버그를 가지고 있습니다
전형적인 에이전트 루프 (Agentic loop):
- 사용자가 에이전트에게 주문을 요청함
- 에이전트가 MCP 또는 A2A를 통해
create_order를 호출함 - 응답이 도착하기 전에 네트워크가 끊김
- 에이전트는 작업이 성공했는지 알 수 없음
- 에이전트가
create_order를 재시도함 - 두 개의 주문. 두 번의 결제. 화가 난 고객 한 명.
이것은 이론적인 예외 상황(edge case)이 아닙니다. 도구 호출(tool-call) 도중에 네트워크 타임아웃(network timeout)이 발생하거나, 긴 작업 중에 컨테이너가 재시작되거나, 속도 제한(rate limit)이 걸려 에이전트가 물러났다가 재시도하거나, 혹은 LLM이 재샘플링(re-samples)하여 이미 호출했던 도구를 다시 실행할 때마다 프로덕션 환경에서 실제로 발생합니다.
에이전트는 이전의 브라우저와 마찬가지로 자신의 마지막 동작이 성공했는지 알지 못합니다. 그래서 다시 시도합니다.
매핑 (The Mapping)
해결책은 동일한 해결책입니다. 이름이 바뀌고 계층이 바뀌었을 뿐, 문제는 변하지 않았습니다.
| 웹 PRG | 에이전트 대응 방식 |
|---|---|
| POST — 위험함, 비멱등적 (non-idempotent) | 상태를 변경하는 도구 호출 (Mutating tool call) — charge_card, send_email, create_record |
| ... |
멱등성 키(idempotency key)가 바로 리다이렉트(redirect)입니다. 이것이 "그 일을 수행하라"와 "이미 수행했을 때 우리가 얻은 것을 반환하라"를 구분해 줍니다.
에이전트가 create_order를 호출할 때, 다음과 같은 키를 전달해야 합니다:
idempotency_key = hash(user_id + "place_order" + cart_id)
서버는 확인합니다: 이 키를 이전에 본 적이 있는가? 예 — 저장된 결과를 반환합니다. 아니요 — 처리하고, 저장하고, 응답합니다.
이제 에이전트는 백 번이라도 재시도할 수 있습니다. 첫 번째 호출 이후의 모든 호출은 키를 확인하고, 결과를 찾아 반환합니다. 아무것도 두 번 실행되지 않습니다.
규칙은 PRG와 동일합니다. 키는 반드시 재시도 루프(retry loop)
_위(above)_에 존재해야 합니다. 첫 번째 시도 전, 요청의 타임스탬프가 아닌 사용자의 의도와 같이 안정적인 무언가로부터 키를 한 번만 생성하십시오. 매 재시도마다 바뀌는 키는 PRG에서 새로고침할 때마다 새로운 폼 URL을 생성하는 것이 무의미한 것과 마찬가지로, 전체 목적을 무너뜨립니다.
PRG가 끝나고 에이전트가 더 어려워지는 지점
PRG는 진입점(entry point)을 보호합니다. 사용자의 관점에서 웹 폼은 원자적(atomic)입니다. 즉, 성공했거나 혹은 실패했거나 둘 중 하나입니다.
에이전트 기반 작업(agentic task)은 20분 동안 실행될 수도 있고, 10개의 서로 다른 도구(tool)를 사용할 수도 있으며, 중간에 충돌(crash)이 발생할 수도 있습니다. 이 상황에서 진입점에 설정된 멱등성 키(idempotency key)는 아무런 도움이 되지 않습니다. 당신에게 필요한 것은 체크포인팅(checkpointing)입니다. 즉, 각 의미 있는 단계마다 상태(state)를 저장하여, 재시작 시 처음부터 다시 시작하는 대신 중단된 지점부터 다시 시작할 수 있도록 하는 것입니다.
이를 모든 단계에 재귀적으로 적용된 PRG라고 생각하십시오. 각 단계는 자신만의 멱등성 키를 가집니다. 다음 단계로 넘어가기 전에 각 단계의 결과가 저장됩니다. 재시작 시에는 단계를 다시 실행하는 대신 완료된 단계들을 다시 읽어 들입니다.
이것이 바로 Temporal의 내구 실행 모델(durable execution model)이 기계적으로 수행하는 방식입니다. 재시작 시 이벤트 히스토리(event history)를 재생(replay)하지만, 이미 기록된 단계는 건너뛰므로 부수 효과(side effect)가 두 번 발생하지 않습니다. 각 단계가 커밋(commit)되면 전체 실행은 안전한 GET 요청들의 시퀀스가 됩니다.
내구성을 갖춘 에이전트(durable agents)를 위한 전체 그림은 다음과 같습니다:
- 작업 진입 시 멱등성 키 (Idempotency key at task entry) — 중복 작업 생성을 방지합니다.
- 단계별 체크포인트 (Step-level checkpoints) — 작업 중간에 완료된 단계가 재실행되는 것을 방지합니다.
- 모든 상태 변경 도구 호출 시 멱등성 키 (Idempotency keys on every mutating tool call) — 가장 안쪽의 보호 계층입니다.
만약 단 하나만 할 수 있다면, 세 번째를 선택하십시오. 외부 상태를 기록하는 모든 도구가 두 번 호출되어도 안전하다면, 운영 환경(production)에서의 많은 고통을 피할 수 있습니다.
이것이 생략되는 이유
서버 측 프레임워크들이 PRG를 가장 먼저 흡수했습니다. 'POST 후 리다이렉트(redirect-after-POST)'가 기본 경로가 되면서 아무도 이를 고민할 필요가 없게 되었습니다. 그 후 클라이언트 측 JavaScript가 이를 무의미하게 만들었습니다. XHR 콜백이든, jQuery deferred이든, 혹은 당신이 선호하는 어떤 프레임워크의 async/await이든, 모든 상태 변경(mutation)은 브라우저의 탐색 히스토리(navigation history)를 전혀 건드리지 않는 독립적인 작업이 되었습니다. 다시 재생할 페이지 자체가 없다면, POST 재전송(POST-replay)에 대해 생각하는 것을 멈추게 됩니다.
에이전트 프레임워크(Agent frameworks)는 아직 이를 흡수하지 못했습니다. MCP에는 멱등성 키(idempotency keys)에 대한 내장 개념이 없습니다. A2A의 작업 수명 주기(task lifecycle)는 단계별 체크포인팅(step-level checkpointing)을 강제하지 않습니다. 프레임워크는 아직 초기 단계이며, 이러한 패턴들은 실제 운영 환경에서 — 때로는 고통스럽게 — 발견되고 있습니다. 보통 사용자가 비용을 두 번 지불하거나, 이메일이 세 번 발송되거나, 아무도 설명할 수 없는 중복 레코드로 데이터베이스가 가득 찬 후에야 말이죠.
이것은 계속해서 재발견되고 있습니다
흥미로운 점은, 그 누구도 이 모든 것들이 하나로 수렴할 것이라고 계획하지 않았다는 것입니다.
웹 개발자들은 2000년대 초반에 PRG에 도달했습니다. 브라우저가 이 문제를 강제했기 때문입니다. 폼 재전송(form replay)은 요청 흐름을 재구성하는 것 외에는 해결 방법이 없는, 사용자에게 눈에 보이는 버그였습니다.
Temporal은 분산 시스템(distributed systems)의 관점에서 이 문제에 접근했습니다. 그들의 내구 실행 모델(durable execution model)은 재시작 시 워크플로 히스토리(workflow history)를 재생하지만, 이미 기록된 단계는 건너뜁니다. 프레임링(framing)은 다르지만 근본적인 보장(guarantee)은 동일합니다. 즉, 이미 발생한 부수 효과(side effect)는 다시 발생하지 않는다는 것입니다.
그리고 프로덕션 SaaS 시스템에서 85개의 도구를 운영하는 엔지니어들이 작성한 2026년 논문 — Agent-First Tool APIs — 는 에이전트 신뢰성(agent reliability) 방향에서 이 결론에 도달했습니다. 그들은 모든 도구의 디스크립터(descriptor)에 idempotency_key_fields를 선언된 필드로 추가했습니다. 이는 권장 사항이 아니라 계약(contract)의 필수적인 부분입니다. 이를 통해 키가 어떻게 유도되는지에 대한 문제는 장애 발생 시가 아니라 설계 단계에서 해결됩니다.
서로 다른 문제를 다루는 세 개의 별개 커뮤니티가 동일한 구조적 해답에 도달했습니다. 그러한 패턴은 대개 주의를 기울일 가치가 있습니다.
배포하기 전에
외부 상태(external state)를 변경하는 모든 에이전트 도구의 경우:
- 모든 상태 변경(mutating) 도구가 멱등성 키(idempotency key)를 허용합니까?
- 키가 요청 타임스탬프가 아닌 사용자의 의도(intent)로부터 유도됩니까?
- 키가 첫 번째 시도 전에 생성되어 모든 재시도(retry) 시 재사용됩니까?
- 서버가 결과를 저장하고 중복된 키에 대해 해당 결과를 반환합니까?
- 장기 실행 작업(long-running tasks)의 경우: 중간 단계들이 체크포인트(checkpointed)로 저장됩니까?
리다이렉트(redirect)가 멱등성 키(idempotency key)입니다. GET은 저장된 결과입니다. 동일한 패턴입니다. 다른 계층(layer)일 뿐입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
