
에이전트 출력물을 위한 거버넌스가 적용된 이중 전송 방지(Double-Send-Safe) 전달 파이프라인 구축하기
요약
자율 에이전트 시스템이 이메일이나 문서 등을 중복 전송하거나 승인 없이 발송하는 문제를 방지하기 위한 거버넌스 기반의 전달 파이프라인 구축 가이드를 제공합니다. 외부 부수 효과 발생과 시스템 기록 사이의 간극을 메우기 위해 인간의 승인과 트랜잭션 구조를 활용하는 설계 패턴을 설명합니다.
핵심 포인트
- 에이전트의 중복 전송은 작업 완료 기록 전 시스템 충돌 시 발생하는 구조적 문제임
- 모든 외부 발송은 인간의 승인 게이트와 증거 영수증을 갖춘 트랜잭션으로 취급해야 함
- 생성, 영속화, 승인 등 단계별 내구성을 갖춘 6단계 파이프라인 설계 권장
- 실패 시 차단(fail-closed) 방식과 재전송을 거부하는 충돌 복구 경로 구축 필요
저는 소규모 비즈니스를 운영하기 위해 멀티 에이전트 시스템 (multi-agent system)을 구축했습니다. 에이전트들은 업무 초안을 작성했고, 그 업무 중 일부는 외부로 전달되었습니다: 실제 사람들에게 보내는 이메일, 내보내기 된 문서, 전달된 결과물(artifacts) 등입니다. 이후 시장 상황으로 인해 비즈니스를 종료했지만, 전달 파이프라인 (delivery pipeline)만큼은 단 한 줄의 코드도 바꾸지 않고 다시 구축하고 싶은 부분입니다. 이 파이프라인은 대부분의 에이전트 데모가 생략하는 질문에 답을 줍니다: 어떻게 하면 자율 시스템 (autonomous system)이 중복 전송하거나, 오작동하거나, 아무도 승인하지 않은 것을 발송하지 않으면서 사람을 대신해 무언가를 보낼 수 있게 할 것인가?
이 가이드는 해당 비즈니스가 운영하던 구체적인 내용은 제거하고 일반화한 패턴입니다. 이는 고객 이메일, 외부 메시지, 내보내기 된 기록 등, 에이전트가 중요한 결과물을 전송하는 모든 사람을 위한 것입니다. 즉, "두 번 전송됨" 또는 "승인 없이 전송됨"이 단순한 미관상의 버그가 아니라 실제 비용(cost)으로 발생하는 경우를 위한 것입니다.
한 줄 요약: 모든 외부 발송 동작을 인간의 승인 게이트 (human approval gate)와 증거 영수증 (evidence receipt)을 갖춘 내구성이 있고, 임대된(leased) 형태이며, 실패 시 차단되는(fail-closed) 트랜잭션 (transaction)으로 취급하십시오. 그리고 충돌 복구 (crash-recovery) 경로가 기본적으로 재전송을 거부하도록 만드십시오. 이 가이드의 나머지 내용은 이 문장을 상세히 풀어낸 것입니다.
이메일을 보내는 에이전트는 왜 이메일을 두 번 보낼까요?
이중 전송 (double-send)은 드문 예외 케이스가 아닙니다. 작업자 (worker)가 전송 도중에 종료될 때 발생하는 단순한 큐 (naive queue)의 기본 동작입니다. 작업자가 전송 작업 (send task)을 할당받아 메시지를 메일 서버에 전달한 후, "완료(done)"를 기록하기 전에 (메모리 부족, 배포, 재부팅 등으로) 종료되는 경우입니다. 작업은 여전히 미완료 상태로 남아 있으므로, 다음 작업자가 이를 가져가서 다시 전송합니다. 고객은 이메일을 두 통 받게 되고, 당신은 고객 지원 티켓을 받게 됩니다.
아래의 모든 안전한 전달 (safe-delivery) 결정은 바로 그 틈을 메우기 위해 존재합니다. 실패는 운이 나빠서가 아니라 구조적인 문제입니다. 가장 위험한 순간은 "외부 세계에서 부수 효과(side effect)가 발생한 시점"과 "시스템이 그것이 발생했다고 기록한 시점" 사이의 간극입니다. 그 간극에서 충돌 (crash)이 발생할 때 재시도 (retry)가 중복 전송이 됩니다. 그 간극을 제로(0)로 만들 수는 없으므로, 그 안에서 충돌이 발생할 상황을 대비하여 설계해야 합니다.
거버넌스가 적용된 전달 파이프라인 (governed delivery pipeline)은 어떤 모습인가요?
파이프라인은 하나의 중추(spine)로 구성되며, 규칙은 어떠한 중대한 전송(consequential send)도 승인(approval), 단일 전송(single-send), 그리고 증명(attest) 게이트를 건너뛰어서는 안 된다는 것입니다. 점수 산정(score) 및 검토(review) 단계는 모든 전송을 차단하기보다는 사람이 어떤 항목을 먼저 읽을지를 결정합니다. 다음 단계가 시작되기 전 각 단계가 내구성을 갖는 6단계 과정은 다음과 같습니다:
- 생성 (Produce). 에이전트가 결과물(artifact)을 완전히 생성하고(이메일의 경우 완전한 RFC 5322 메시지) 이를 스테이징(staging)합니다. 생성 시점에 인라인(inline)으로 전송되는 것은 아무것도 없습니다.
- 영속화 (Persist). 프로세스에서 무언가가 나가기 전에 의도(intent)(스테이징된 결과물, 큐 행(queue row), 영수증 포함)를 내구성이 있는 저장소(durable storage)에 기록합니다. 지금 시스템이 충돌하더라도 의도는 생존하며 검사가 가능합니다.
- 점수 산정 (Score). 출력물에 신뢰도 점수(confidence score)를 부착합니다. 이는 검토 라우팅(review routing)을 위한 입력값이며, 그 자체로 게이트 역할을 하지는 않습니다.
- 검토 (Review). 신뢰도가 낮은 항목을 사람의 검토 큐(human review queue)로 라우팅합니다. 신뢰도가 높은 항목이라도 다음 단계를 건너뛰지는 않습니다.
- 승인 (Approve). 실패 시 차단(fail-closed) 방식의 승인 게이트입니다. 릴리스는 기본적으로 차단되며, 명시적이고 기록된 권한 부여(authorization)가 있을 때만 진행됩니다.
- 전송 및 증명 (Send and attest). 리스(lease) 하에 정확히 한 번만 전송한 다음, 발생한 상황을 설명하는 증거 영수증(evidence receipt)을 조정(reconcile)하고 기록합니다.
중요한 속성은 2단계부터 6단계까지가 하나의 함수 호출이 아니라 별개의 내구성이 있는 전이(durable transitions)라는 점입니다. 각 단계는 작업의 손실이나 중복 없이 충돌 후 재개될 수 있는데, 이는 중요한 상태(state)가 워커(worker)의 메모리가 아닌 저장소(store)에 존재하기 때문입니다.
워커가 종료될 때 전송을 멱등(idempotent)하게 만드는 방법은 무엇인가요?
이 단계는 대부분의 팀이 실수하는 부분이며, 해결책은 라이브러리가 아니라 규칙에 있습니다. 행 수준 리싱 (row-level leasing)을 사용하여 한 번에 정확히 하나의 워커(worker)만이 작업을 소유하도록 하고, 리스 (lease) 만료 시 외부 전송 (external-send) 액션은 재큐잉 (requeue) 할 수 없도록 만드세요. Postgres에서 리싱 프리미티브 (leasing primitive)는 SELECT ... FOR UPDATE SKIP LOCKED 하의 클레임 (claim)입니다. 동시 실행되는 워커들이 서로 다른 행을 선택하고 결코 동일한 행을 선택하지 않으므로, 두 명의 워커가 동시에 전송할 수 없습니다.
리스 (lease)는 크래시 복구 (crash recovery)를 안전하게 만드는 핵심이며, 리퍼 (reaper)는 실제 결정이 내려지는 곳입니다. 리퍼는 워커가 사망하여 리스가 만료된 작업들을 회수합니다. 일반적인 내부 작업의 경우, 회수하여 다시 실행하는 것이 올바릅니다. 하지만 외부 전송 (external send)의 경우 이는 장전된 총과 같습니다. 만약 메일 서버가 메시지를 수락한 후 성공을 기록하기 전에 워커가 사망했다면, 재실행 시 중복 전송이 발생합니다. 따라서 규칙은 다음과 같습니다:
외부 전송 (external-send) 액션은 리스 만료 시 재큐잉 (requeue) 할 수 없습니다. 리스가 만료되면 리퍼는 해당 작업을 다시 실행하는 대신, 사람이 조정할 수 있도록 작업을 고립(strand)시킵니다.
이것이 전체 트릭입니다. 보이지 않는 중복 전송보다는 눈에 보이는 멈춘 작업이 낫습니다. 안전함을 증명할 수 없는 자동 복구 대신, 메시지가 실제로 발송되었는지 확인하여 사람이 몇 초 안에 해결할 수 있는 수동 복구를 선택하는 것입니다. 부작용이 없는 (side-effect-free) 내부 작업은 자동 재큐잉 (auto-requeue) 상태를 유지하며, 외부 세계와 접촉하는 액션만 고립됩니다. 작업에 액션 유형을 인코딩하여 리퍼가 이를 기준으로 분기하도록 하세요. 사람이 그 차이를 기억하는 것에 의존하지 마십시오.
파이프라인을 페일 클로즈(fail-closed) 상태로 유지하는 방법은 무엇인가요?
페일 클로즈 (fail-closed)란 안전한 상태가 기본값이며, 모든 불안전한 액션은 진행을 위해 명시적이고 긍정적인 신호를 필요로 함을 의미합니다. 세 가지 기본 설정이 대부분의 비중을 차지합니다:
- 전송은 켜질 때까지 비활성화됨(Sending is off until switched on). 단일 전달 활성화 플래그가 모든 아웃바운드 전송을 제어하며, 기본값은 비활성화 상태입니다. 잘못된 설정, 새로운 환경, 또는 부분 배포된 릴리스는 잘못 보내기보다는 아무것도 보내지 않습니다.
- 발신자 신원을 가정하지 않고 확인함(The sender identity is checked, not assumed). 전송 시점에서 'from' 주소는 검증된 전송 도메인과 비교되며, 암호화된 전송이 예상되지만 연결이 비암호화 또는 잘못된 포트로 폴백될 경우 트랜스포트 가드(transport guard)가 전송을 거부합니다. 이러한 확인 절차는 설정 주석에 있는 것이 아니라 전송 코드 자체에 구현되어 있습니다.
- 기록된 승인 없이는 중요한 것은 배포되지 않으며, 배포되는 모든 것에는 영수증이 남습니다(Nothing consequential ships without a recorded approval, and everything that ships leaves a receipt). 승인 게이트가 병목 지점이며, 영수증이 감사 추적(audit trail)입니다. 결과가 사람이 확인하기 전까지는 진정으로 알 수 없기 때문에, 고립되고 조정되지 않은 전송은 의도적으로 성공 영수증 없이 남겨집니다.
운영상의 짧은 참고 사항을 공유합니다. 저에게 비용이 들었기 때문입니다. 한 번 삼켜진 임포트 오류(import error)가 사전 비행 점검(preflight check)을 약 65일 동안 조용히 고장 상태로 만들었습니다. 재부팅을 통해 마침내 이 문제가 드러났고, 그 시간 내내 실패는 양성적인 '종속성 사용 불가' 상태로 읽혔습니다. 정상 상태처럼 보이는 억제된 오류가 그 자체로 하나의 실패 모드입니다. 폐쇄(fail-closed) 원칙은 폐쇄 상태가 명확할 때만 도움이 됩니다.
실제로 이것이 필요한 경우와 과도한 경우
잘못된 전송에 대한 피해 범위(blast radius)에 맞춰 장치를 매칭시키십시오. 낮은 위험도의 내부 출력물(채널의 초안, 로그 라인, 사람이 명백히 보고 수정할 제안 등)의 경우, 이 전체 파이프라인은 과도하며 구축해서는 안 됩니다. 의식적인 절차를 거치는 비용이 실수로 인한 비용보다 큽니다.
잘못된 전송이나 중복 전송이 실제 비용을 발생시키는 순간, 당신에게는 이 시스템이 필요합니다. 고객 대상 이메일, 금전과 관련된 모든 것, 컴플라이언스(Compliance) 또는 법적 기록이 남는 모든 것, 그리고 수신자가 그에 따라 행동하는 모든 것이 이에 해당합니다. 이러한 경우 파이프라인은 부가적인 장식(gold-plating)이 아니라 반드시 갖춰야 할 최소한의 기준(floor)입니다. 판단 기준은 간단합니다. 만약 "두 번 전송됨" 또는 "승인 없이 전송됨"이 고객 지원 티켓, 환불, 또는 법적 책임(liability)을 발생시킨다면, 당신은 적용 범위(in scope) 안에 있는 것입니다.
이 패턴은 제가 이를 구축했던 비즈니스보다 더 오래 살아남았으며, 이것이 이 패턴이 특정 제품이 아닌 이곳에 존재하는 솔직한 이유입니다. 해당 시스템을 은퇴시키기로 한 결정, 그리고 깨끗한 아키텍처(architecture)가 비즈니스 자금을 계속 투입하는 이유가 아니라 패턴을 문서화해야 하는 이유라는 원칙은, 동반 에세이인 "Architecture Fit Is Not Business Justification"에서 별도로 다루고 있습니다. 전송 결과가 중대한 영향을 미칠 때 파이프라인을 구축하십시오. 그렇지 않다면 건너뛰십시오. 그리고 무엇을 구축하든, 재전송을 시도하는 주체(reaper)가 기본적으로 재전송을 거부하도록 만드십시오.
원문은 danmercede.com에 게시되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기