두 에이전트가 서로 통신할 수 없나요? 제가 사용하는 디버깅 체크리스트
요약
서로 다른 환경에서 실행되는 두 에이전트 간의 통신 실패를 해결하기 위한 디버깅 체크리스트를 제공합니다. 데몬 실행 여부, 상호 신뢰 핸드셰이크, NAT 환경에서의 네트워크 도달 가능성을 중심으로 단계별 점검 방법을 설명합니다.
핵심 포인트
- 데몬 프로세스가 정상적으로 실행 중인지 가장 먼저 확인해야 합니다.
- 에이전트 간 신뢰 핸드셰이크가 양방향으로 올바르게 이루어지는지 점검하십시오.
- NAT 환경에서는 UDP 트래픽 차단 가능성을 고려하여 릴레이 설정을 검토해야 합니다.
- 연결 실패 시 에러가 발생하지 않는 '침묵' 상태는 네트워크 계층의 문제일 확률이 높습니다.
당신은 Agent A를 작성하여 클라우드 VM에 배포합니다. Agent B를 작성하여 당신의 노트북에서 실행합니다. Agent A가 메시지를 보냅니다. 아무런 응답이 없습니다. 에러도, 타임아웃도 없습니다 — 그저 침묵뿐입니다.
이런 일은 제가 인정하고 싶을 만큼 여러 번 발생했습니다. 두 에이전트가 서로에게 도달할 수 없을 때, 근본 원인은 거의 항상 사소한 것입니다: 누락된 핸드셰이크 (handshake), UDP를 조용히 드롭하는 NAT, 또는 — 당혹스럽게도 자주 발생하는 — 시작되지 않은 데몬 (daemon) 같은 것들 말이죠. 하지만 실패 모드가 항상 동일하기 때문에 ("응답 없음"), 결국 잘못된 계층(layer)에서 유령을 쫓게 됩니다.
두 에이전트가 왜 서로에게 도달할 수 없는지 디버깅해야 할 때마다 제가 수행하는 순차적 체크리스트를 소개합니다. 이는 시간을 몇 시간이나 절약해 줍니다.
1단계: 데몬 (daemon)이 실제로 실행 중인가?
이것은 이 목록에서 가장 창피한 확인 사항이며, 바로 그렇기 때문에 첫 번째로 배치되었습니다. 저는 대상 머신에서 에이전트의 데몬을 시작조차 하지 않았다는 사실을 깨닫기 위해 연결 실패를 디버깅하며 20분을 허비한 적이 있습니다.
ps aux | grep daemon
아무것도 나타나지 않는다면, 시작하세요. 만약 시작되자마자 즉시 종료된다면 로그를 확인하세요 — 포트 충돌 (port conflict)이나 누락된 설정 파일 (config file)이 일반적인 원인입니다. 새로운 배포(deploy) 시에는 데몬이 프로비저닝 스크립트 (provisioning script)를 거치며 살아남았는지 확인하십시오.
2단계: 신뢰가 상호적인가?
대부분의 에이전트 간 네트워킹 시스템은 어떤 형태의 신뢰 핸드셰이크 (trust handshake)를 사용합니다. 만약 Agent A는 Agent B를 신뢰하지만 Agent B가 Agent A를 승인하지 않았다면, 트래픽은 한 방향으로만 흐르게 되며, 이는 정확히 "응답 없음"과 같은 모습으로 나타납니다.
이것은 P2P (peer-to-peer) 설정에서 비대칭적 연결 실패 (asymmetric connectivity failures)가 발생하는 가장 흔한 원인입니다. 수신자는 핸드셰이크 요청을 받지만, 이를 무시하고 아무것도 다시 보내지 않습니다.
자신만의 신뢰 계층 (trust layer)을 구축하고 있다면, 세 가지가 필요합니다: (a) 대역 외 (out-of-band) 신원 교환, (b) 각 측이 상대방의 승인을 확인할 수 있는 방법, (c) 오래된 대기 중인 요청에 대한 타임아웃 (timeout). 이 중 하나라도 잘못되면 메시지는 조용히 사라집니다.
3단계: NAT를 통해 실제로 서로를 볼 수 있는가?
이것은 매우 어려운 문제입니다. 만약 두 에이전트가 모두 NAT 뒤에 있다면 — 서로 다른 네트워크에 있는 에이전트들, 예를 들어 하나는 클라우드 VM에 있고 하나는 집 WiFi에 있는 경우라면 거의 항상 그렇습니다 — 표준 UDP는 도움 없이는 작동하지 않습니다.
NAT 문제는 세 가지 하위 점검 항목으로 나뉩니다:
어느 한 쪽 에이전트라도 요청되지 않은 트래픽(unsolicited traffic)을 수신할 수 있는가? 만약 에이전트가 대칭형 NAT (Symmetric NAT) 또는 캐리어 등급 NAT (Carrier-grade NAT) 뒤에 있다면, 릴레이 (Relay) 없이는 어떤 들어오는 UDP도 도달할 수 없습니다. 모든 NAT 유형에 작동하는 미들박스 펀칭 (Middlebox punching) 기술은 없습니다. 서버 매개 홀 펀칭 (Server-mediated hole-punching)과 협조하지 않는 케이스를 위한 릴레이 폴백 (Relay fallback)이 필요합니다.
릴레이를 설정했는가? 릴레이가 없다면, 대칭형 NAT 뒤에 있는 두 에이전트는 절대 연결될 수 없습니다. 릴레이가 있다면, 이들은 보장된 전달을 위해 몇 밀리초(ms)의 추가 지연 시간 (Latency)을 감수하게 됩니다.
NAT 매핑이 만료되었는가? UDP NAT 바인딩 (Bindings)에는 타임아웃 (Timeout)이 있습니다. TCP 지원 NAT의 경우 종종 30120초이며, 순수 UDP 흐름의 경우 때로는 15초만큼 낮을 수도 있습니다. 만약 에이전트가 메시지를 하나 보내고 1분 동안 침묵한다면, 매핑이 끊어지고 응답은 갈 곳을 잃게 됩니다. 1030초마다 실행되는 킵얼라이브 핑 (Keepalive pings)이 이를 방지합니다.
4단계: 동일한 와이어 포맷 (Wire format)을 사용하고 있는가?
이 문제는 운영상의 버그가 아니라 설계상의 버그이기 때문에 뼈아픕니다. 에이전트 A는 어제 생성한 스키마 (Schema)를 사용하여 protobuf로 직렬화 (Serialize)합니다. 에이전트 B는 지난달에 게시된 명세서 (Spec)에 따른 JSON 와이어 포맷을 기대합니다. 둘 다 실행됩니다. 둘 다 스스로를 "에이전트"라고 부릅니다. 하지만 어느 쪽도 상대방이 보낸 것을 파싱 (Parse)할 수 없습니다.
해결책은 공유된 계약 (Shared contract)입니다. 두 에이전트가 동일한 SDK를 사용한다면 이 기능은 무료로 제공됩니다. 만약 그렇지 않다면 — 하나는 Python이고 다른 하나는 Go라면 — 양측이 독립적으로 구현해야 하는 명시적인 와이어 포맷과, 언어를 가로질러 메시지가 왕복(round-trip)하는 것을 확인하는 테스트 하네스 (Test harness)가 필요합니다.
5단계: 와이어(Wire)에서 무엇이 보이는가?
1단계부터 4단계까지의 점검을 통과했는데도 메시지가 여전히 도착하지 않는다면, 패킷 (Packets)을 살펴보세요.
tcpdump -i any port <agent-port> -X
당신이 찾아야 할 것은 다음과 같습니다:
- 패킷이 전혀 도착하지 않음 — 패킷이 0개라면 문제는 에이전트가 아니라 네트워크 경로(network path)에 있습니다.
- 패킷은 도착하지만 RST 또는 ICMP unreachable을 받음 — 포트가 리스닝(listening) 상태가 아닙니다.
- 손상된 페이로드 (Corrupted payload) — 프레임이 잘렸거나(truncated) 인코딩(encoding)이 잘못되었습니다.
패킷 트레이스(packet trace)는 거짓말을 하지 않기 때문에, 이 확인 절차만으로 대부분의 디버깅 세션이 종료됩니다. 만약 ICMP Port Unreachable이 보인다면, 프로세스가 리스닝 중이 아니었던 것입니다. 다시 1단계로 돌아가세요.
체크리스트 자체가 문제인 경우
체크리스트를 가지고 있는 것은 좋지만, 매번 체크리스트가 필요하다는 것은 애초에 문제가 존재해서는 안 된다는 신호입니다.
제가 원했던 것은 에이전트를 위한 네트워킹 레이어(networking layer)였습니다. 1단계(데몬 라이프사이클, daemon lifecycle)는 프레임워크 차원에서 보장되고, 2단계(신뢰, trust)는 양측에서 단일 명령어로 해결되며, 3단계(NAT traversal)는 STUN, 홀 펀칭(hole-punching), 그리고 자동 릴레이 폴백(automatic relay fallback)을 통해 투명하게 처리되고, 4단계(와이어 포맷, wire format)는 버전 간에 어긋나지 않는 SDK 계약(contract)이 되는 그런 레이어 말입니다.
그것이 바로 Pilot Protocol이 하는 일입니다. 에이전트를 위해 구축된 오픈 소스 오버레이 네트워크(overlay network)입니다. 각 에이전트는 재시작, IP 변경, NAT 환경에서도 유지되는 영구적인 가상 주소를 할당받습니다. 연결은 상호 핸드셰이크(mutual handshakes)를 통해 인증됩니다. 암호화된 UDP 터널은 STUN, 홀 펀칭, 그리고 직접 통신이 불가능할 때의 릴레이 폴백을 통해 자동으로 구축됩니다. Go, Python, Node, Swift용 SDK는 언어 간에 와이어 포맷이 동일하게 유지됨을 보장합니다.
배포하는 에이전트 쌍마다 이 체크리스트를 실행하는 데 지쳤다면 다음과 같이 실행하세요:
curl -fsSL https://pilotprotocol.network/install.sh | sh
pilotctl daemon start
pilotctl handshake <peer> "let's talk"
그렇지 않다면, 이 목록을 출력해서 모니터에 붙여두세요. 분명 필요하게 될 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기