A2A 푸시 알림: 명세(Spec)가 보장하는 것과 사용자에게 맡기는 것
요약
Agent2Agent(A2A) 프로토콜의 푸시 알림 명세와 구현 방식을 다룹니다. 긴 작업 시간을 처리하기 위해 웹훅(Webhook) 기반의 비동기 알림 구조를 제안하며, 인증 및 보안 가이드라인을 상세히 설명합니다.
핵심 포인트
- SSE 대신 웹훅을 사용하여 장기 실행 에이전트 작업의 연결 문제를 해결함
- PushNotificationConfig를 통해 URL, 토큰, 인증 방식을 설정 가능
- 태스크의 8가지 상태 변화(종료 및 중간 상태)에 따른 알림 제공
- SSRF 방지 및 서명 검증 등 양방향 보안 가이드라인 제시
Agent2Agent 프로토콜은 실질적인 문제를 해결했습니다. 오래 지속되는 에이전트(Agent) 작업은 요청(Request)과 응답(Response) 구조 안에 담기 적합하지 않으며, SSE 스트림을 40분 동안 열어두는 방식은 의존하기에 바람직하지 않습니다. 따라서 A2A는 클라이언트가 웹훅(Webhook)을 등록하고 연결을 해제할 수 있게 하며, 원격 에이전트가 알 가치가 있는 사건이 발생했을 때 다시 게시(Post)하도록 합니다.
명세(Specification)는 다루는 부분에 대해 신중하게 설계되었습니다. 또한, 명세가 다루지 않는 부분에 대해서는 생략을 통해 명시적으로 나타내고 있는데, 바로 그 부분이 새벽 3시에 당신의 통합(Integration)이 제대로 작동할지 여부를 결정하는 부분입니다.
명세가 실제로 제공하는 것
푸시 알림은 PushNotificationConfig를 통해 설정됩니다. 필드들은 작고 합리적입니다:
url: POST 요청을 받을 HTTPS 엔드포인트(Endpoint)token: 수신자가 해당 알림이 자신이 알고 있는 작업(Task)에 속하는지 확인하기 위해 검사할 수 있는 선택적인 불투명한 값(Opaque value)authentication: 송신 에이전트가 당신의 웹훅에 어떻게 인증할지를 설명하는 선택적 세부 정보. 초기SendMessage또는SendStreamingMessage호출 시 인라인(Inline)으로 제공하거나, 기존 작업에 대해 별도로 관리할 수 있습니다. 현재 명세는 이러한 설정들에 대해 생성(Create), 조회(Get), 목록(List), 삭제(Delete) 작업을 노출하므로, 클라이언트는 작업을 재시작하지 않고도 엔드포인트를 교체할 수 있습니다.
전송 시(On the wire), 알림 본문은 스트리밍(Streaming)과 동일한 StreamResponse 형태를 사용하며, task, message, statusUpdate, 또는 artifactUpdate 중 정확히 하나를 포함합니다. 이미 스트리밍 소비자(Consumer)를 작성했다면, 웹훅 소비자(Consumer)의 대부분을 이미 구현한 셈입니다.
또한 명세(Spec)는 언제 알림을 기대할 수 있는지에 대해서도 알려줍니다. 서버가 결정하지만, 가이드라인은 중요한 상태 변화(State changes)가 발생할 때 알림이 발생한다는 것입니다. 태스크(Task)는 8가지 상태 중 하나에 있습니다. 4가지는 종료 상태(Terminal states)입니다: COMPLETED, FAILED, CANCELED, REJECTED. 나머지 4가지는 중간 상태(Interim states)입니다: SUBMITTED, WORKING, INPUT_REQUIRED, AUTH_REQUIRED. 실제로 여러분은 종료 상태 알림과 함께, INPUT_REQUIRED 및 AUTH_REQUIRED 상태에서 알림을 받게 될 것입니다. 왜냐냐하면 이 두 가지는 태스크가 중단되어 사용자의 조치를 기다리는 중간 상태이기 때문입니다.
인증(Authentication) 측면에서도 매우 유용합니다. Bearer 토큰, API 키, HMAC 서명, mTLS가 모두 명시되어 있으며, 키 배포를 위한 JWT 및 JWKS 예시도 포함되어 있습니다. 보안 가이드라인은 양방향 모두에서 훌륭합니다. 송신 에이전트(Sending agent)는 클라이언트가 제공한 URL을 적대적인 것으로 간주해야 하며, 도메인 허용 목록(Allowlisting), 소유권 검증(Ownership verification), 송신 제어(Egress controls)를 통해 SSRF(Server-Side Request Forgery)에 대비해야 합니다. 수신 웹훅(Webhook)은 신뢰할 수 있는 키를 통해 서명을 검증하고, 설정된 경우 token을 유효성 검사하며, 타임스탬프(Timestamps)와 논스(Nonces) 또는 jti 클레임(Claims)을 사용하여 재전송 공격(Replay attacks)을 차단해야 합니다.
이는 매우 잘 정의된 프로토콜입니다. 이를 읽으면 알림이 어떤 모습인지, 그리고 알림이 주장하는 발신처로부터 온 것임을 어떻게 증명할 수 있는지 정확히 알 수 있습니다.
명세에 없는 문장
A2A의 어디에도 웹훅(Webhook)이 다운되었을 때 어떤 일이 발생하는지에 대해서는 언급되어 있지 않습니다.
재시도 정책(Retry policy)도 없습니다. 백오프(Backoff) 가이드라인도 없습니다. 타임아웃 임계값(Timeout threshold)도 없습니다. 송신 에이전트가 몇 번의 시도를 해야 하는지, 혹은 시도를 해야 하는지 여부에 대한 진술도 없습니다. 최소 한 번 전달(At-least-once) 또는 최대 한 번 전달(At-most-once)에 대한 정의도 없습니다. 명세는 알림이 HTTP POST를 통해 전달된다고만 명시할 뿐, 그 단어 이후의 모든 것은 구현(Implementation)의 몫으로 남겨둡니다.
이는 방어 가능한 선택입니다. 전달 의미론(Delivery semantics)을 강제하려는 프로토콜은 시간이 흐를수록 문제가 생기는 경향이 있기 때문입니다. 하지만 이는 A2A를 기반으로 구축하는 모든 이들에게 특정한 의미를 갖습니다: 여러분의 에이전트 알림의 신뢰성은 프로토콜의 속성이 아닙니다. 그것은 반대편에 있는 구현체의 속성이며, 여러분은 아마도 그 구현체의 소스 코드를 읽어보지 않았을 것입니다.
A2A 규격을 준수하는 두 에이전트는 이 지점에서 완전히 다르게 동작할 수 있습니다. 한 에이전트는 지수 백오프 (exponential backoff)를 사용하여 6시간 동안 10번 재시도합니다. 다른 에이전트는 한 번 시도한 뒤 예외 (exception)를 포착하여 로그를 남기고 다음 단계로 넘어갑니다. 두 방식 모두 규격을 준수하는 것입니다. 여러분의 통합 (integration) 테스트는 어느 쪽을 대상으로 해도 통과됩니다.
이것이 일반적인 웹훅 (webhook)보다 에이전트에게 더 치명적인 이유
order.paid 웹훅이 누락되는 것은 좋지 않은 일입니다. A2A 최종 알림 (terminal notification)이 누락되는 것은 더 나쁜 일이며, 그 이유는 구조적입니다.
푸시 알림 (push notifications)은 정확히 장시간 실행되는 케이스를 위해 존재합니다. 클라이언트가 의도적으로 연결을 끊은 것입니다. 그것이 바로 기능의 핵심입니다. 따라서 에이전트가 40분간의 실제 연산 (compute)이 소요된 작업을 완료했을 때, 이를 알리는 POST 요청은 단 하나뿐인 경우가 많으며, 더 이상 그 연결을 지켜보는 사람은 없습니다.
만약 그 POST 요청이 실패하고 재시도되지 않는다면, 세 가지 사실이 동시에 발생합니다. 작업은 수행되었습니다. 결과는 원격 에이전트 (remote agent)에 존재합니다. 그리고 여러분 측 시스템은 해당 작업을 상태를 변경해 줄 알림이 유실되었기 때문에, 영원히 WORKING 상태라고 믿게 됩니다.
스트리밍 (streaming) 방식이었다면 연결 끊김을 감지했을 것입니다. 그것이 여러분이 푸시 방식을 선택했을 때 감수한 트레이드오프 (trade-off)입니다. 좋은 선택이지만, 감지해야 하는 부담을 전달 계층 (delivery layer)으로 옮긴 것입니다.
이보다 더 고약한 2차적인 버전도 있습니다. 송신자가 알림 시점을 결정하기 때문에, 에이전트는 동일한 상태 전이 (state transition)를 두 번 보내는 것이 정당할 수 있으며, 재시도하는 송신자는 분명히 그렇게 할 것입니다. 만약 여러분의 웹훅 핸들러 (webhook handler)가 멱등성 (idempotent)을 보장하지 않는다면, 중복된 COMPLETED 알림은 단순히 로그가 두 번 남는 것으로 끝나지 않습니다. 여러분의 완료 핸들러 (completion handler)가 실행하는 모든 동작을 두 번 실행하게 됩니다.
수신 측의 의무
여러분의 웹훅은 제3자가 POST 요청을 보내는 공개 HTTPS 엔드포인트 (endpoint)입니다. 그에 걸맞게 취급하십시오.
신뢰하기 전에 검증하십시오. 서명(signature)이나 베어러 토큰(bearer token)을 예상되는 키와 대조하고, token 필드를 등록된 값과 비교하십시오. 이때 타이밍 공격을 통해 한 번에 1바이트씩 자격 증명(credential)이 유출되지 않도록 반드시 상수 시간 비교(constant-time comparison)를 사용하여 두 작업을 수행하십시오. 불일치 시에는 401을 반환하되, 어느 부분이 실패했는지는 설명하지 마십시오.
재전송 공격(replays)을 거부하십시오. 타임스탬프 윈도우(timestamp window)와 이미 확인된 논스(seen-nonce) 집합만으로도 충분합니다. JWT를 사용 중이라면 이를 위해 jti 클레임(claim)이 존재합니다.
멱등성(idempotent)을 유지하고, 올바르게 키를 지정하십시오. A2A 알림의 자연 키(natural key)는 무작위 메시지 ID가 아니라, 보고되는 상태와 결합된 작업 ID(task ID)여야 합니다. 왜냐하면 여러분이 단 한 번만 발생하기를 원하는 사건은, 해당 소식을 전달하는 POST 요청이 몇 번이든 상관없이 "이 작업이 COMPLETED 상태가 되었다"는 사실이기 때문입니다.
빠르게 응답하십시오. 알림이 여러분 측에 내구적으로 기록(durably written)되는 즉시 2xx로 승인한 다음, 실제 작업은 비동기적(asynchronously)으로 수행하십시오. 세 개의 내부 서비스를 호출하느라 11초가 걸리는 핸들러는 결국 송신 측의 타임아웃(timeout)보다 더 오래 걸리게 될 것이며, 결과적으로 이미 완료한 작업에 대해 재시도(retry)를 받게 됩니다.
송신 측의 의무
만약 여러분이 에이전트(agent)를 실행하는 측이라면, 알림은 여러분의 아웃바운드 전달(outbound delivery) 문제이며, 이는 다른 모든 아웃바운드 전달 문제와 정확히 동일합니다.
전송하기 전에 기록하십시오. POST를 시도하기 전에 상태 전이(state transition)가 여러분의 데이터베이스에 내구적으로 기록되어 있어야 합니다. 그렇지 않으면 "작업 완료"와 "알림 전송" 사이에서 프로세스가 충돌(crash)할 경우, 알림을 보내야 했다는 유일한 기록을 잃게 됩니다.
재시도하되, 판단력을 가지고 재시도하십시오. 연결 시간 초과(connection timeout)나 503 에러는 엔드포인트(endpoint)가 곧 복구될 가능성이 높으므로, 지터(jitter)를 포함한 백오프(backoff) 일정에 따라 다시 시도할 가치가 있습니다. 반면 422나 400 에러는 재시도할 가치가 없습니다. 10분 뒤에도 페이로드(payload)는 여전히 잘못되어 있을 것이며, 당신이 하는 일은 누군가가 이 통합(integration)이 배포 시점부터 망가져 있었다는 사실을 발견하는 순간을 늦추는 것뿐이기 때문입니다. 410 Gone은 수신 측에서 중단하라고 말하는 것이며, 올바른 대응은 계속 시도하는 것이 아니라 설정을 비활성화하는 것입니다.
가시적인 곳에서 실패하십시오. 재시도 예산(retry budget)이 소진되면, 알림은 사람이 실제로 읽을 수 있는 데드 레터 큐(dead letter queue)에 도달해야 합니다. 이때 로그 라인(log line) 형태가 아닌 전체 데이터가 그대로 저장되어야, 엔드포인트가 복구되었을 때 누군가 이를 다시 재생(replay)할 수 있습니다. 개별 알림이 아니라 엔드포인트에 대해 경고(alert)를 설정하십시오. 그렇지 않으면 모든 사람이 경고를 무시하도록 만들게 될 것입니다.
URL을 방어하십시오. 클라이언트는 임의의 HTTPS 주소를 당신에게 건네며 당신의 네트워크 내부에서 그곳으로 POST 요청을 보내달라고 요청합니다. 이는 전형적인 SSRF(Server-Side Request Forgery) 벡터이며, 이것이 바로 명세(spec)에서 이를 지적하는 이유입니다. 허용 목록(allowlist)을 사용하고, 소유권을 확인하며, 송신(egress)을 통제된 상태로 유지하십시오.
솔직하게, 직접 만들 것인가 살 것인가
위의 내용 중 특이한 것은 하나도 없습니다. 유능한 백엔드 팀이라면 영속성 큐(persisted queue), 지터(jitter)가 포함된 재시도 워커(retry worker), 데드 레터 테이블(dead letter table), 그리고 재생 엔드포인트(replay endpoint)를 구축할 수 있습니다. 팀들은 매 분기마다 이를 수행합니다.
비용은 구축 자체에 있는 것이 아닙니다. 실패 모드가 '침묵'인 인프라의 소유권을 이제 당신이 떠맡게 되었다는 점, 그리고 사용자들이 실제로 비용을 지불하고 있는 에이전트(agent)를 구축하는 동안 이 인프라가 올바르게 작동하도록 유지해야 한다는 점이 문제입니다. 전달(delivery) 인프라는 고장 났을 때 당신에게 호출(page)을 보내지 않습니다. 그저 무언가를 언급하는 것을 멈출 뿐입니다.
만약 당신이 단 하나의 에이전트를 실행하고 있으며, 당신의 팀이 운영하는 단 하나의 엔드포인트(endpoint)에 알림을 보내는 상황이라면, 직접 재시도 루프(retry loop)를 작성하십시오. 그것이 해당 문제에 필요한 적절한 엔지니어링 수준입니다. 주의 깊게 살펴봐야 할 기준선은 처리량이 아닙니다. 바로 상대방(the other end)을 누가 소유하고 있는가입니다. 일단 다른 사람들이 운영하는 엔드포인트에 메시지를 게시하기 시작하면, 그들의 가동 시간(uptime)과 배포 일정(deploy schedule)에 종속되게 됩니다. 이때부터 실패 모드(failure modes)는 더 이상 당신의 통제 하에 있지 않으며, 당신은 고객을 통해서야 비로소 그 사실을 알게 됩니다.
요약 버전
A2A는 푸시 알림(push notification)이 어떤 모습인지, 그리고 그것이 어디에서 왔는지 어떻게 증명하는지를 알려줍니다. 하지만 POST 요청이 실패했을 때 어떤 일이 발생하는지는 알려주지 않으며, 그 공백은 연결의 양측 모두가 직접 채워야 합니다.
전송하기 전에 상태 전이(state transition)를 영속화(persist)하십시오. 재시도 가능한 것은 재시도하고, 그 외의 모든 것에는 빠르게 중단하십시오. 수신 측은 태스크 ID(task ID)와 상태(state)를 기준으로 멱등성(idempotent)을 보장해야 합니다. 왜냐하면 중복 전송은 송신자의 버그가 아니라, 송신자가 자신의 임무를 수행하는 과정이기 때문입니다. 자격 증명(credentials)은 상수 시간(constant time) 내에 검증하고 재전송 공격(replays)은 거부하십시오. 그리고 끝내 전달되지 못한 알림들은 사람이 확인할 수 있는 어딘가에 기록해 두십시오. 작업이 영원히 조용히 WORKING 상태로 남아 있는 것이야말로, 그 누구도 대시보드를 만들어 모니터링하지 않는 유일한 실패 모드이기 때문입니다.
Mittr는 A2A 푸시 알림을 포함하여, 웹훅(webhooks) 및 AI 에이전트 액션을 위한 신뢰할 수 있는 액션 레이어(action layer)입니다. Mittr는 명세(spec)에 정의된 Bearer 토큰과 OIDC 또는 OAuth2 JWT라는 두 가지 인증 프리셋(auth presets)을 모두 처리하며, 상수 시간 자격 증명 검사 및 불일치 시 401 에러를 반환합니다. 모든 수신 내역은 요청 검사기(request inspector)에 기록되며, 멱등성 키(idempotency keys)는 태스크와 상태로부터 유도되고, 알림은 태스크 ID별로 상관관계(correlated)가 설정됩니다. 송신 측의 경우, 모든 액션은 전달 전에 Postgres에 기록되며, 엔드포인트별 서킷 브레이커(circuit breaker) 뒤에서 지터(jitter)를 포함한 선행 스케줄(front-loaded schedule)에 따라 재시도됩니다. 또한 재시도 횟수(budget)를 모두 소진하면 메시지를 버리는 대신 데드 레터(dead-lettered)로 처리합니다. 무료 티어는 카드 등록 없이 월 3,000개의 메시지를 제공합니다. 자세한 내용은 mittr.io에서 확인하세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기