[WebRTC 딥다이브] STUN, TURN, ICE를 헷갈리지 않게 정리하기
WebRTC 연결 로그를 처음 오래 들여다봤을 때 가장 헷갈린 단어가 STUN, TURN, ICE였다. 셋 다 연결을 도와주는 것처럼 보였고 실제 문서에서도 거의 붙어 다닌다. 그래서 초반에는 STUN 서버를 중계 서버처럼 말하거나, TURN을 넣었으니 ICE 과정은 대충 넘어가도 된다고 생각했다. 로그에 host, srflx, relay 후보가 같이 찍히기 시작하자 그 착각이 바로 드러났다. 이름이 비슷해서가 아니라 각자 맡은 일이 다른데 내가 한 덩어리로 부르고 있었던 것이다. 숫자로 포장하기보다, 재현 조건과 운영에서 고정한 규칙을 중심으로 적었다.

STUN/TURN/ICE를 헷갈린 이유
ICE 상태 이름도 처음에는 건조하게 보이지만 운영에서는 꽤 중요하다. new, checking, connected, completed, failed, disconnected 같은 상태가 화면 문구와 1:1로 붙지는 않는다. 그래도 상태 전이가 어디서 멈췄는지 알면 문의 대응이 달라진다. checking에서 오래 머무르면 후보 검사나 네트워크 제한을 의심하고, connected 뒤 전송이 느리면 DataChannel이나 수신 저장 쪽을 봐야 한다. 연결 계층과 전송 계층을 섞지 않는 것이 핵심이다.
팀과 이 주제를 볼 때는 candidate type을 칠판에 따로 적어 본다. host는 내부 주소, srflx는 STUN으로 확인한 외부 주소, relay는 TURN을 지나는 주소다. 이름을 색으로 나누면 “STUN 서버가 데이터를 중계한다” 같은 말이 줄어든다. 용어를 분리하는 일이 곧 장애 대응의 첫 단계가 된다. 마지막으로 다시 볼 질문을 남긴다. STUN과 TURN의 역할을 로그에서 구분하고 있는가. selected candidate type을 저장하고 있는가. TURN credential 갱신과 만료를 테스트했는가. relay 사용률을 비용 지표로 보고 있는가. 이 네 가지에 답할 수 있으면 STUN, TURN, ICE는 외워야 할 약어가 아니라 연결 설계를 점검하는 언어가 된다.
개발 중에는 일부러 TURN만 쓰게 하거나 TURN을 빼고 직접 후보만 보게 하는 실험이 도움이 된다. 정상 경로를 일부러 제한하면 각 구성요소의 책임이 더 잘 보인다. STUN만 있을 때 어떤 네트워크에서 실패하는지, TURN만 강제했을 때 지연과 비용이 어떻게 보이는지, ICE 후보 검사가 실패하면 화면이 어디에서 멈추는지 확인한다. 이런 실험은 출시 전 대규모 QA가 아니라 로컬 공부 단계에서도 할 수 있다.

섞기 쉬운 인접 개념
내가 노트에 적어 두는 최소 지표는 네 가지다. candidate gathering에 걸린 시간, selected candidate pair의 타입, ICE 실패율, TURN 인증 오류다. 여기에 제품에 따라 relay 사용량과 평균 전송량을 붙인다. 파일 전송이라면 bufferedAmount, chunk 재전송, 수신 저장 속도도 같이 봐야 한다. 미디어라면 packet loss와 bitrate 변화가 더 중요하다. 같은 WebRTC라도 무엇을 보내는지에 따라 봐야 할 숫자가 달라진다. 제품 관점에서는 후보 타입을 꼭 남겨야 한다.
연결이 됐는지만 보면 직접 연결과 relay 연결을 구분하지 못한다. 사용자는 둘 다 성공으로 보지만 운영 비용은 다르다. 회사망 사용자 비율이 늘어 relay 선택률이 갑자기 올라가면 서버 요금이 먼저 반응한다. 반대로 연결 실패율만 보고 있으면 후보 수집이 늦은 문제인지, TURN 인증이 만료된 문제인지, 방화벽이 UDP를 막은 문제인지 분리하기 어렵다.
STUN, TURN, ICE를 넣기 전에 정해야 할 것도 있다. 직접 연결 실패를 제품 실패로 볼 것인지, 정상적인 fallback 경로로 볼 것인지부터 정해야 한다. TURN을 허용한다면 어느 크기와 어느 시간까지 허용할지, relay 사용률이 얼마를 넘으면 경고할지, credential TTL을 어떻게 둘지도 필요하다. 이 기준이 없으면 데모는 잘 되는데 운영 비용과 장애 대응은 뒤늦게 따라온다. 디버깅할 때는 브라우저의 WebRTC internals를 열어 candidate pair를 보는 습관이 도움이 된다.
어떤 후보가 선택됐는지, bytes가 어느 경로로 흘렀는지, round trip time이 어떻게 변하는지 확인하면 막연한 추측이 줄어든다. 다만 이 도구를 운영자와 사용자가 볼 수는 없다. 그래서 개발 중에 본 필드를 제품 로그로 어떤 이름으로 남길지 따로 정해야 한다. 사용자에게는 내부 용어를 그대로 보여 줄 필요가 없다. 다만 제품이 지금 무엇을 하는지는 말해야 한다.
"직접 연결을 시도하는 중", "네트워크 제한으로 우회 경로를 확인하는 중", "우회 경로로 연결되어 속도가 달라질 수 있음" 정도면 충분하다. 개발자 로그에는 candidate type과 ICE state가 남고, 화면에는 사용자가 기다릴지 다른 방법을 선택할지 판단할 수 있는 문장이 남는 구조가 좋다.
각 프로토콜이 지키는 것
이 노트의 결론을 짧게 줄이면, STUN은 길을 찾기 위한 주소 힌트이고 TURN은 막힌 길을 돌아가는 유료 도로이며 ICE는 후보 도로를 시험하는 절차다. 셋을 분리해 이해하면 연결 실패를 더 작은 사건으로 나눌 수 있다. 더 작은 사건으로 나누면 사용자 문구, 운영 로그, 비용 지표가 같은 방향으로 맞춰진다. 사용자 문구는 세 용어를 그대로 드러내지 않아도 된다.
대신 “연결 경로를 찾는 중”, “우회 경로로 전환하는 중”처럼 단계가 느껴져야 한다. 내부 로그에는 candidate type과 ICE state가 남고, 화면에는 사용자가 기다려야 하는 이유가 남는다. 두 정보가 이어져야 문의를 읽을 수 있다. 관련해서는 [과 [을 같이 읽으면 좋다. 앞글은 WebRTC의 역할을 과장하지 않는 데 도움이 되고, 뒤 글은 DataChannel 위에 애플리케이션 프로토콜이 왜 필요한지 보여 준다.
위 그림처럼 흐름을 그릴 때는 신호와 payload를 다른 선으로 두는 편이 좋다. 방을 만들고 상대에게 초대 정보를 전달하는 일은 signaling 쪽에서 일어나지만, 파일이나 미디어 바이트가 항상 그 서버를 지나가는 것은 아니다. STUN은 후보를 만들 때 필요한 정보를 주고, ICE는 후보를 비교한다. TURN이 선택된 경우에만 payload가 릴레이 서버를 지난다. 이 선을 섞어 그리면 장애를 볼 때도 엉뚱한 곳만 뒤지게 된다.
브라우저 권한과 탭 상태도 연결 품질에 영향을 준다. 화면 공유나 마이크가 얽힌 미디어 세션에서는 권한 거부가 먼저 실패를 만들 수 있고, 파일 전송에서는 탭이 백그라운드로 내려간 뒤 timer나 저장 동작이 느려질 수 있다. STUN, TURN, ICE를 네트워크 장비 문제로만 보면 이런 브라우저 조건을 놓친다. 제품 로그에는 브라우저, OS, 탭 상태를 과하게 수집하지 않더라도 최소한 사건을 구분할 수 있는 단서가 필요하다.
문서에는 약어보다 의사결정 기준을 남겨야 한다. "relay 비율이 일정 수준을 넘으면 비용 알림을 본다", "TURN 인증 오류가 늘면 credential 발급 서버부터 확인한다", "candidate gathering 시간이 길면 네트워크 제한 문구를 조정한다" 같은 문장이 실제 운영에 더 도움이 된다. 기술 정의는 검색하면 나오지만 제품 기준은 팀이 정하지 않으면 생기지 않는다. WebRTC를 설명할 때 서버/클라이언트/사용자 중 누구 문장으로 말하는지 먼저 고르면 논쟁이 줄었다.
프로토콜 관점에서 본 차이
보안 쪽으로도 구분이 필요하다. STUN 서버 목록은 비교적 단순해 보이지만 TURN credential은 다르다. 누구나 오래 쓸 수 있는 credential을 클라이언트에 넣어 두면 relay 자원을 빼앗길 수 있다. 그래서 실제 서비스에서는 단기 credential을 발급하고, 방 권한과 연결해 쓰고, 만료되었을 때 다시 받을 수 있는 경로를 둔다. 연결 기술을 공부하다 보면 이 대목을 지나치기 쉬운데 운영에서는 비용과 abuse가 바로 붙는다.
PonsLink와 PonsWarp를 생각할 때도 이 구분이 도움이 된다. PonsLink처럼 방, 권한, 요청 상태가 중요한 제품에서는 중앙 서버가 일관성을 잡아야 한다. PonsWarp처럼 파일 바이트 자체를 피어끼리 보내고 싶은 제품에서는 직접 경로가 열릴 때와 막힐 때를 나눠 설명해야 한다. WebRTC가 길을 열어 줄 수는 있지만 모든 네트워크를 설득하지는 못한다. 그래서 Direct, relay, Cloud Drop 같은 선택지는 서로의 승패가 아니라 사용자 환경과 데이터 크기에 맞춘 경로가 된다.
실제 구현에서 한 번 더 조심할 부분은 signaling과 ICE를 같은 문제로 오해하지 않는 것이다. signaling 서버가 offer와 answer를 전달하지 못하면 후보 검사를 시작하기도 어렵다. 반대로 signaling은 정상인데 ICE 후보가 맞지 않아 실패할 수도 있다. 사용자는 둘 다 연결 실패로 보지만 로그에서는 분리해야 한다. 방 생성 실패, 상대 오프라인, 후보 수집 실패, 후보 검사 실패를 같은 에러 코드로 묶으면 나중에 어떤 층을 고쳐야 할지 알 수 없다.
조금 더 풀어 보면 host 후보는 가장 싸고 빠른 길일 가능성이 높지만 같은 네트워크나 단순한 경로에서만 잘 맞는다. srflx 후보는 STUN을 통해 외부에서 보이는 주소를 얻은 결과라 직접 연결의 가능성을 넓힌다. relay 후보는 TURN 서버를 거치므로 성공률을 보강한다. 이 셋은 우열만의 문제가 아니다. 제품은 먼저 싼 길을 시도하고, 실패하면 우회하고, 우회했을 때의 비용과 사용자 안내를 감당해야 한다.
설계할 때 던져야 할 질문
실패를 작게 쪼개는 습관도 필요하다. 사용자가 "연결이 안 된다"고 말할 때 내부 원인은 여러 가지다. 후보가 하나도 모이지 않았을 수 있고, 후보는 모였지만 검사가 실패했을 수 있다. 직접 경로가 막혀 relay로 넘어갔는데 TURN credential이 만료됐을 수도 있다. 연결은 됐지만 relay가 멀어 지연이 커졌을 수도 있다. 이 차이를 로그와 화면 문구에서 구분하지 않으면 지원 문의는 전부 회색 오류가 된다.
STUN을 거울에 비유하면 꽤 오래 기억된다. 브라우저는 사설 IP 뒤에 숨어 있고, 상대는 그 주소만 보고 들어올 수 없다. STUN 서버에 물어보면 인터넷 바깥에서 보이는 주소와 포트를 확인할 수 있다. 다만 거울은 길을 열어 주지 않는다. symmetric NAT나 엄격한 방화벽처럼 목적지마다 매핑이 달라지는 환경에서는 STUN으로 알아낸 후보만으로 연결이 안 될 수 있다.
그래서 STUN을 넣었다는 사실은 직접 연결을 시도할 준비가 됐다는 뜻이지 성공 보장은 아니다. ICE는 이 둘을 포함해 후보를 모으고 검사하는 과정이다. host 후보는 로컬 네트워크에서 바로 닿을 수 있는 주소이고, srflx 후보는 STUN으로 확인한 외부 주소다. relay 후보는 TURN을 통해 우회하는 주소다. ICE는 이 후보들을 모아 연결 검사를 하고 실제로 통하는 쌍을 고른다.
그래서 "ICE 서버"라는 말은 대개 STUN/TURN 서버 목록을 가리키는 편의 표현일 뿐, ICE 자체는 서버라기보다 브라우저와 피어 연결이 수행하는 절차에 가깝다. 자주 하는 실수는 iceServers 설정을 복사해 넣고 연결 문제를 끝낸 것으로 여기는 것이다. 설정은 출발점일 뿐이다. 실제로는 TURN 서버가 어느 region에 있는지, credential 발급 서버가 장애 때 어떻게 동작하는지, 모바일 브라우저에서 탭이 백그라운드로 갔을 때 상태가 어떻게 바뀌는지까지 이어진다.
특히 파일 전송은 미디어보다 사용자가 기다리는 시간이 길어 작은 상태 문구 차이도 크게 느껴진다. 먼저 한 줄로 나누면 이렇다. STUN은 바깥에서 보이는 내 주소를 확인하는 데 쓰인다. TURN은 직접 경로가 막혔을 때 실제 데이터를 대신 전달하는 릴레이 서버다. ICE는 host, server reflexive, relay 같은 여러 후보를 모아 시험하고 그중 쓸 경로를 고르는 절차다. 세 단어를 서버 종류로 외우면 계속 흔들리고, 주소 발견, 우회 전송, 후보 선택이라는 책임으로 보면 훨씬 덜 헷갈린다.
방 크기도 결정에 영향을 준다. 1:1 파일 전송에서는 후보 선택과 relay 비용을 비교적 단순하게 볼 수 있다. 여럿이 동시에 미디어를 주고받는다면 이야기가 달라진다. Mesh 구조에서는 참가자가 늘수록 각 브라우저가 맺어야 할 연결 수와 업로드 부담이 커진다. 이때도 STUN, TURN, ICE만으로 답이 끝나지 않는다. SFU 같은 별도 구조가 필요한지, 어떤 payload를 서버가 중계해야 하는지 다시 물어야 한다.
이어서 보면 좋은 글
STUN, TURN, ICE를 다시 읽을 때는 세 단어를 외웠는지보다 로그가 나뉘어 있는지 확인한다. 주소를 찾는 단계, 후보를 시험하는 단계, relay로 넘어가는 단계를 구분할 수 있어야 한다. 그래야 연결 실패가 났을 때 “WebRTC가 안 된다”가 아니라 어느 장치가 다음 행동을 막았는지 말할 수 있다. ICE 상태 이름도 처음에는 건조하게 보이지만 운영에서는 꽤 중요하다. new, checking, connected, completed, failed, disconnected 같은 상태가 화면 문구와 1:1로 붙지는 않는다.
그래도 상태 전이가 어디서 멈췄는지 알면 문의 대응이 달라진다. checking에서 오래 머무르면 후보 검사나 네트워크 제한을 의심하고, connected 뒤 전송이 느리면 DataChannel이나 수신 저장 쪽을 봐야 한다. 연결 계층과 전송 계층을 섞지 않는 것이 핵심이다. 팀과 이 주제를 볼 때는 candidate type을 칠판에 따로 적어 본다. host는 내부 주소, srflx는 STUN으로 확인한 외부 주소, relay는 TURN을 지나는 주소다.
이름을 색으로 나누면 “STUN 서버가 데이터를 중계한다” 같은 말이 줄어든다. 용어를 분리하는 일이 곧 장애 대응의 첫 단계가 된다. 마지막으로 다시 볼 질문을 남긴다. STUN과 TURN의 역할을 로그에서 구분하고 있는가. selected candidate type을 저장하고 있는가. TURN credential 갱신과 만료를 테스트했는가. relay 사용률을 비용 지표로 보고 있는가. 이 네 가지에 답할 수 있으면 STUN, TURN, ICE는 외워야 할 약어가 아니라 연결 설계를 점검하는 언어가 된다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.