[NAT Traversal] NAT traversal은 왜 실패할 수밖에 없는가
NAT traversal이라는 말을 처음 보면 NAT를 뚫는 기술처럼 들린다. 실제로는 그렇게 단단한 약속이 아니다. 사설망 뒤에 있는 두 장치가 서로 닿을 수 있는 후보를 찾고, 포트 매핑을 이용해 보고, 안 되면 릴레이로 돌아가는 시도에 가깝다. 이 차이를 놓치면 "WebRTC를 쓰면 직접 연결이 된다"는 말이 너무 쉽게 나온다. 좋은 네트워크에서는 맞아 보이지만 회사망, 학교망, 호텔 Wi-Fi, 일부 모바일망에서는 바로 깨진다. Traversal만 있으면 나머지는 따라올 거라고 믿었다.

NAT traversal가 막힌 지점
마지막 판단은 늘 사용자 시간으로 돌아온다. 내부적으로 어떤 후보가 실패했는지보다 사용자가 몇 초를 기다렸고, 그 뒤 무엇을 할 수 있었는지가 제품의 기억으로 남는다. 이 차이를 남겨 두면 다음 구현도 덜 흔들린다. NAT traversal을 다시 볼 때는 성공률 숫자만 보지 않는다. 어떤 NAT 유형에서 막혔는지, UDP가 차단됐는지, relay 후보가 왜 선택됐는지까지 남겨야 한다.
이 정보가 없으면 사용자는 같은 실패를 반복하고 운영자는 네트워크 탓이라는 말만 되풀이하게 된다. 사용자 문구는 기술명보다 행동 가능성을 우선해야 한다. "NAT traversal 실패"라고 보여 주는 대신 "현재 네트워크에서 직접 연결이 어려워 우회 경로를 확인하고 있습니다"가 낫다. 우회 경로가 열렸다면 속도나 제한이 달라질 수 있음을 알려야 한다.
완전히 막혔다면 다시 초대할지, 네트워크를 바꿀지, cloud fallback으로 넘길지 선택지를 줘야 한다. 실패를 숨기면 사용자는 기다림을 고장으로 이해한다. UDP 제한도 자주 놓친다. WebRTC는 보통 UDP 경로를 선호하지만 모든 네트워크가 UDP를 너그럽게 열어 주지는 않는다. 방화벽이 특정 포트를 막거나, UDP를 아예 제한하거나, 짧은 시간 동안만 매핑을 유지할 수 있다.
이때 TCP나 TLS 기반 TURN으로 돌아갈 수는 있지만 지연과 비용이 달라진다. 사용자는 왜 갑자기 느려졌는지 모르므로 제품이 "우회 경로"라는 말을 준비해야 한다.

처음에는 주소만 바꾸면 될 줄 알았다
binding timeout은 연결이 된 뒤에 나타나서 더 얄밉다. 처음에는 잘 연결되었는데 잠시 아무 데이터도 오가지 않거나 탭이 백그라운드로 내려간 뒤 경로가 사라진다. keepalive를 어떻게 둘지, 재연결을 언제 시도할지, 재연결 중 사용자에게 무엇을 보여 줄지 정하지 않으면 실제 사용 중 끊김이 늘어난다. 연결 성공 화면만 테스트하면 이 문제를 보기 어렵다.
테스트 시나리오는 작게라도 표로 남기는 편이 좋다. 같은 와이파이 1:1, 서로 다른 가정망, 모바일 핫스팟, VPN, 회사망에 가까운 제한 환경, 오래 켜 둔 탭, 화면 잠금 후 복귀처럼 적는다. 모든 환경을 당장 자동화하지 못해도 이름이 있으면 기능을 설명할 때 빠뜨리지 않는다. 나중에 문의가 들어와도 어느 칸의 실패인지 짚을 수 있다. NAT traversal은 보안 장비와 줄다리기를 하는 일이기도 하다.
방화벽은 이유 없이 까다로운 것이 아니라 내부망을 보호하려고 제한한다. 제품은 그 제한을 몰래 우회하는 태도로 접근하면 안 된다. 허용되는 경로를 확인하고, 실패하면 relay나 저장 기반 경로로 전환하고, 필요한 경우 관리자에게 열어야 할 도메인이나 포트를 안내하는 식이 더 현실적이다. NAT traversal을 공부하다 보면 "그럼 TURN을 항상 쓰면 되지 않나"라는 질문도 나온다.
가능은 하지만 답은 제품마다 다르다. TURN은 성공률을 높이지만 모든 payload를 서버로 돌린다. 미디어 서비스라면 지연과 대역폭 비용이 커지고, 파일 서비스라면 큰 파일 하나가 비용 이벤트가 된다. 직접 연결 실패를 모두 TURN으로 덮을지, 큰 파일은 cloud fallback으로 넘길지, 무료 사용자는 어느 범위까지 허용할지 정책이 필요하다. 같이 읽을 글로는 [과 [을 남긴다.
NAT traversal을 네트워크 마법으로 보지 않고, 제품 경로 선택 문제로 보는 데 도움이 된다.

실패가 선명해진 순간
NAT traversal 테스트는 일부러 불친절한 환경을 넣어야 한다. 호텔 Wi-Fi, 학교망, 기업 방화벽, 모바일 테더링은 각자 다른 실패를 만든다. 한 환경에서 성공한 candidate pair가 다른 곳에서도 통한다는 보장은 없다. 그래서 QA 표에는 네트워크 종류와 선택된 candidate type을 같이 남겨야 한다. 지원 문서도 이 관점을 따라가야 한다. 사용자가 회사망에서 실패했을 때 "브라우저를 새로고침하세요"만 반복하면 해결되지 않는 경우가 많다.
네트워크 정책상 직접 연결이 제한될 수 있고, 제품이 우회 경로를 시도한다는 설명이 있어야 한다. 조직 네트워크를 쓰는 사용자라면 방화벽이나 VPN 정책이 영향을 줄 수 있다는 안내도 필요하다. 다만 사용자를 탓하는 문장이 되지 않게 조심해야 한다. PonsWarp의 Direct 전송도 이 관점에서 봐야 한다. 직접 연결이 열리면 서버가 파일 바이트를 들지 않아도 되어 좋다.
하지만 직접 경로가 막힌 사용자를 제품 밖으로 밀어내면 안 된다. 큰 파일에서는 TURN relay만으로 버티기 어렵고, Cloud Drop 같은 저장 기반 보완책이 필요할 수 있다. PonsLink 역시 방과 권한은 중앙에서 잡되, 실제 연결 경로의 실패는 사용자 환경을 고려해 설명해야 한다. NAT 실패를 화면에 그대로 설명할 필요는 없다. 다만 “현재 네트워크에서 직접 연결이 어려워 우회 경로를 확인 중” 정도의 문장은 필요하다.
내부에는 NAT 유형 추정, candidate 실패, TURN 사용 여부가 남아야 한다. 그래야 지원 응대가 막연한 재시도 안내로 끝나지 않는다. 결국 NAT traversal 실패를 받아들이는 태도가 중요하다. 실패를 부끄러운 예외로 숨기면 사용자는 이유 없는 고장을 경험한다. 실패를 정상적인 네트워크 조건 중 하나로 두면 제품은 더 차분해진다. 직접 경로가 열리면 빠르게 가고, 막히면 우회하고, 우회도 어렵다면 다른 전송 방식을 제안한다.
이 정도의 솔직함이 실시간 네트워크 기능을 오래 버티게 한다. 다시 볼 질문은 네 가지다. NAT 유형별 시나리오를 문서화했는가. UDP 제한 환경을 테스트하거나 최소한 재현 계획을 세웠는가. 연결 유지 keepalive와 timeout을 확인했는가. relay 또는 cloud fallback을 제품 흐름으로 설명하고 있는가. 이 질문에 답할 수 있어야 NAT traversal 실패를 예외가 아니라 설계 가능한 상태로 다룰 수 있다.
NAT를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
NAT 폐색을 경계로 두기
실험도 좋은 환경 하나로 끝내면 안 된다. 같은 노트북 두 대를 같은 와이파이에 두면 거의 모든 설계가 좋아 보인다. 최소한 다른 네트워크 두 개, 모바일 핫스팟 하나, VPN 또는 회사망에 가까운 제한 환경 하나를 따로 생각해야 한다. 실제 QA 장비를 모두 갖추지 못하더라도 시나리오 이름은 남겨 둬야 한다. "UDP 제한 환경", "binding timeout", "relay 전환", "오래 열린 탭"처럼 이름을 붙이면 나중에 로그와 UI 문구를 설계하기 쉽다.
흐름을 볼 때는 "주소를 알았다"와 "들어올 수 있다"를 분리해야 한다. STUN으로 외부 주소를 알아냈다고 해서 방화벽이 UDP를 허용한다는 뜻은 아니다. NAT binding이 충분히 오래 유지된다는 뜻도 아니다. 오래 켜 둔 탭에서 연결이 갑자기 끊기거나, 모바일망에서 잠깐 화면을 껐다 켠 뒤 경로가 바뀌는 이유도 이 주변에 있다. NAT traversal은 네트워크 장비의 정책과 시간 제한을 상대해야 하므로 성공을 보장하기 어렵다.
로그 설계에서는 내부 원인과 사용자 문구를 분리한다. 내부 원인은 udp_blocked, candidate_timeout, relay_selected, turn_auth_failed처럼 짧고 재사용 가능한 이름이 좋다. 사용자 문구는 그보다 부드러워야 한다. "현재 네트워크에서 직접 연결이 제한되어 우회 경로를 사용합니다" 정도면 충분하다. 기술적으로 정확하면서도 사용자를 탓하지 않는 문장이어야 한다. 마지막으로 NAT traversal은 성공률 하나로 평가하기보다 실패가 어느 선택지를 남겼는지까지 봐야 한다.
직접 경로가 안 붙어도 TURN이 준비되어 있으면 사용자는 작업을 끝낼 수 있고, TURN도 없다면 화면은 빨리 포기해야 한다. 이 차이를 제품이 알고 있어야 불필요한 재시도와 긴 침묵을 줄일 수 있다. 특히 NAT에서는 재현 조건을 같이 적어야 의미가 있었다.
메커니즘이 버틴 이유
내가 보는 축은 NAT 유형, UDP 허용 여부, binding timeout, fallback이다. NAT 유형은 직접 경로 가능성을 크게 바꾼다. UDP 제한은 WebRTC의 기본 경로를 흔든다. timeout은 연결 후 안정성에 영향을 준다. fallback은 사용자가 결국 파일이나 미디어를 받을 수 있는지를 결정한다. 이 네 가지를 한 표에 놓고 보면 NAT traversal이 단순 네트워크 기능이 아니라 제품 약속과 이어진다는 점이 보인다. NAT는 내부 주소와 외부 주소 사이를 바꿔 주는 장치다.
집 공유기처럼 단순한 환경에서는 한 번 밖으로 나간 연결의 응답을 다시 안으로 들여보내기 쉽다. 문제는 모든 NAT가 같은 규칙을 쓰지 않는다는 데 있다. 어떤 NAT는 목적지가 바뀌어도 비슷한 매핑을 유지하고, 어떤 NAT는 목적지마다 다른 외부 포트를 만든다. symmetric NAT가 까다로운 이유가 여기에 있다. 상대가 STUN으로 확인한 주소를 보고 들어오려 해도, 그 주소가 상대에게는 더 이상 맞는 문이 아닐 수 있다.
모바일에서는 배터리와 네트워크 전환도 함께 온다. 사용자가 LTE에서 Wi-Fi로 바꾸거나 화면을 잠그면 경로가 바뀔 수 있다. 긴 파일 전송이나 긴 통화에서는 처음 연결보다 유지와 복구가 더 중요해진다. NAT traversal을 연결 시작 단계로만 보면 이 부분이 빠진다. keepalive, 재연결, 사용자 취소, 이어받기 정책까지 한 흐름으로 봐야 한다. 어떤 값을 기록할지, 어떤 제한을 문서화할지, 어떤 fallback을 정상 흐름으로 볼지 정한다.
이 정도까지 내려오면 기술 약어가 제품의 약속으로 바뀐다. 반대로 여기까지 내려오지 않으면 글은 그럴듯하지만 다음 장애 때 다시 검색창을 열게 된다. 특히 NAT에서는 재현 조건을 같이 적어야 의미가 있었다. 작은 서비스라면 처음부터 모든 NAT 유형을 완벽히 분류하지 못할 수 있다. 그래도 후보 타입, ICE state, relay 전환 여부만 남겨도 다음 판단이 쉬워진다.
어느 순간부터 특정 네트워크에서 실패가 늘어나는지, relay 전환은 성공했지만 속도 불만이 커지는지, 실패 후 재시도가 실제로 도움이 되는지 볼 수 있다. 관찰이 먼저 있고 최적화가 뒤따라야 한다. '[NAT Traversal NAT traversal은 왜 실패할 수밖에 없는가'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.