WebRTC 시그널링 서버 개발 : 내부망 기기끼리 어떻게 서로를 찾을까 본문
배경
재밌는 일이 들어왔다.
회사의 카메라 기기를 외부 플랫폼에서 스트리밍 될 수 있도록 만들어야 했다.
회사에 DevOps가 따로 없다 보니 클라우드를 직접 만지고 비용을 관리한다.
특히나 우리 회사는 홈 카메라도 판매하기에 스트리밍을 위한 데이터 통신비가 얼마나 큰지 안다.
WebRTC를 위한 모든 데이터가 서버를 거친다면, 그 속도도 문제지만 통신 비용도 클 것이다.
그래서 노드 간 직접 연결이 중요하다.
나는 여기가 재밌었다. 이 글 자체도 사실 이걸 얘기하고 싶었다.
배포되지 않은 로컬 네트워크의 기기끼리 어떻게 서로를 찾아 직접 통신하는지 궁금했다.
NAT 동작 과정
P2P 통신의 원리를 이해하기 위해선, NAT의 동작 과정을 이해해야 한다.
NAT 내부에서 외부에 요청을 보내면, NAT는 연결 정보를 기억했다가 외부의 응답을 다시 내부 기기로 전달한다.
내부 앱에서 외부 서버 (142.250.197.206:443)으로 요청을 했다고 하자.
NAT는 내부 연결 정보가 겹치지 않도록 임의의 출발지 포트 (58829)를 만들어 외부 서버에 요청한다.
외부에서 해당 포트에 응답이 오면 요청 정보 테이블을 확인하여, 내부 앱의 IP와 포트를 확인하고 이를 전달한다.
요청 정보에 기록된 포트는 허락하고, 기록되지 않은 포트는 거부한다.
요청 정보를 기억하고 요청 통로의 응답은 허용하는 것이 포인트다.

NAT 홀 펀칭
그럼 요청한 경로로 반대쪽에서도 데이터를 전송하면 어떨까?
NAT는 이를 응답으로 착각하고, 매핑 테이블에 따라 내부 앱에 전달할 것이다.
NAT가 요청 to 응답에 같은 경로를 사용함을 이용해서 데이터를 전달하는 방법을 홀 펀칭이라고 한다.
카메라의 실시간 영상을 모바일 앱으로 전달하는 시나리오로 정리해 보자.
서로 다른 로컬 네트워크에 모바일 앱과 카메라가 각자의 외부 IP와 빈 포트를 알고 있다는 전제이다.
1. 모바일 앱이 카메라 기기의 IP:PORT로 UDP 빈 패킷을 전달한다.
2. 카메라 기기는 해당 경로로 스트리밍을 위한 데이터 프레임을 전달한다.
3. NAT는 2번의 패킷을 1번에 대한 응답으로 생각해 이를 모바일 앱에 전달할 것이다.
4. 1 ~ 3을 계속 반복하면서 모바일 앱은 전달 받은 데이터 프레임을 화면에 끊임없이 출력한다.
NAT 출발 경로를 확인하는 방법
이런 홀 펀칭이 가능하려면, 요청 출발 정보를 상대방에게 알려줘야 할 것이다.
NAT에서 나가는 포트 정보는 NAT가 결정하기 때문에, 내부 앱에서는 알 방법이 없다.
내 요청이 NAT로부터 어떻게 전달되는지 알려주는 외부 서버가 STUN 서버이다.
아래는 구글의 Stun 서버를 사용했을 때 예시이다.
공유기의 외부 IP와 함께 NAT가 사용한 포트를 확인할 수 있다.
이 정보를 상대방에 넘겨 데이터 통로로 사용할 수 있도록 하는 것이다.
$turnutils_stunclient stun.l.google.com
0: : IPv4. UDP reflexive addr: 49.x.x.38:40573
0: : IPv4. UDP reflexive addr: 49.x.x.38:40573
P2P가 불가능한 환경에서의 스트리밍
홀 펀칭을 모든 NAT에서 할 수 있는 것은 아니다.
요청마다 혹은 목적지마다 사용 포트가 바뀌는 NAT이거나, 보안이 엄격한 네트워크 환경에선 불가능하다.
Turn 서버는 이런 직접 연결이 불가능한 상황에서 사용하는 중계 서버이다.
목적지마다 사용 포트가 바뀌는 Symmetric NAT가 대표적이다.
Stun 서버에 요청한 포트와 P2P 상대방에게 사용되는 포트가 달라지니 홀 펀칭이 불가능해진다.
이때 Turn 서버를 사용하면 고정된 목적지를 갖기에 NAT 가 매핑 통로를 만들 수 있다.
송신 측은 데이터를 Turn 서버에 전달하고, 수신 측은 Turn 서버의 세션 통로를 열어두고 데이터를 수신하는 것이다.

한쪽만 홀 펀칭이 가능한 경우
아래는 개발을 마치고 테스트 중에 발견한 특이 케이스 로그이다.
Relay는 홀 펀칭이 불가능해서 중계 서버를 사용, Srflx는 홀 펀칭이 가능한 환경이라는 의미이다.
(추가로 Host 타입은 NAT와 외부 네트워크 없이, 내부 IP만으로도 P2P 통신이 가능한 상황을 의미한다.)
이런 경우 한쪽은 P2P, 다른 쪽은 Turn 서버를 사용하는 것이 아니라, 결국 둘 다 Turn 서버를 사용하게 된다.
즉 홀 펀칭이 가능한 쪽도 결국 Turn 서버로 데이터를 전달한다.
A의 최적의 후보지가 Relay, B의 최적의 후보지가 Srflx라고 하자.
B가 A로 데이터를 전달하기 위해선 Turn 서버로 전달할 수밖에 없다.
A가 B로 데이터를 전달할 땐, B의 최적 후보대로 외부 IP로 던지려 할 것이다.
근데 A의 최적의 후보지가 Relay이라는 말은 A는 NAT 홀 펀칭이 안 된다는 말이다.
그래서 B의 Srflx 주소로 데이터를 전달해도 B에 도달하지 못하고, 결국 Turn 서버를 사용하게 되는 것이다.
[09:24:22 PM] ================================================================
[09:24:22 PM] CONNECTION ESTABLISHED VIA: Local: relay <-> Remote: srflx
[09:24:22 PM] - Local: 3.***.139.79:59716 (udp)
[09:24:22 PM] - Remote: 211.***.187.123:45930 (udp)
[09:24:22 PM] ================================================================
시그널링 서버의 역할
사실 이런 Stun, Turn 서버만 있다고 WebRTC 기반 통신이 가능한 것은 아니다.
Stun, Turn 정보, 어떤 코덱이나 암호화를 사용할지, 어떤 네트워크 경로가 가장 효율적인지 등 노드 간 대화가 필요하다.
WebRTC 연결 수립을 위해 사용자 단과 카메라 기기는 다음과 같은 대화 흐름의 필요하다.
1. [FE -> CAM] SDP Offer : 코덱, 미디어 정보, 암호화 방식 등의 스트리밍 정보, 네트워크 경로 후보군 전달
2. [CAM -> FE] SDP Answer : OFFER의 요구사항에 처리 가능한 기기 환경을 응답하고, 미디어 스트림 준비
3. [FE -> CAM] ICE Candidate : FE에서 사용할 수 있는 네트워크 경로(ICE) 후보 전달
4. [CAM -> FE] ICE Candidate : 기기에서 사용할 수 있는 네트워크 경로 (ICE) 후보 전달
이때 3,4번의 통신 경로 후보군은 한 번씩 전달하고 끝나는 것이 아니라, 후보군마다 여러 번 전송한다.
또 통신이 끊기거나 네트워크 환경이 변경되었을 때도 동적으로 전달하여 최적의 경로를 찾아가야 한다.
이때 사용자 단과 기기가 직접 sdp 값, Ice Candidate를 주고받지 않는다.
보안을 위해 Mqtt 브로커 접근은 외부 노출 없이, 중앙 서버만 처리하는 꼴이 안전하다.
시그널링 서버는 사용 단과 기기 사이에서, 사용자 세션, MQTT 송수신을 처리한다.
시그널링 서버와 사용자 간 직접 통신
시그널링 서버에서 사용자 단으로 직접 데이터를 전달되어야 하는 경우, 보통 웹 소켓이나 SSE가 사용된다.
폴링은 실시간성이 떨어지고, 요청-응답이 많아져 서버 부하로 이어지기 좋다.
SSE 서버의 연결을 지속해야 하는지는 판단이 필요하다.
연결을 지속하는 경우, 네트워크나 WebRTC 커넥션에 이상이 생겼을 때 빠르게 ICE를 주고받기 유리하다.
연결을 지속하지 않는 경우, 관리 포인트가 크게 준다는 장점이 있다.
서비스가 네트워크 이상에 얼마나 빠르게 대응해야 하고, SSE 세션 지속 관리 포인트를 어떻게 가져갈지에 대한 선택이 필요하다.
SSE 연결을 지속하는 것이 좋아 보이지만, 상황에 따라 WebRTC 커넥션을 새로 맺어 버리는 것이 더 깔끔할 수도 있겠다.

시그널링 서버에서 플랫폼 측으로 Callback
또는 사용자와의 통신 중간에 플랫폼 측 서버가 놓일 때도 있다.
이 경우에선 앱과 플랫폼 측 서버가 통신하고, 시그널링 서버는 플랫폼 측에 SDP Answer와 ICE 후보지들을 전달한다.
플랫폼 서버 측에서 요청을 전달할 때 후에 응답을 수신할 callback url과 auth key를 함께 전달한다.
플랫폼 측의 요청을 수신한 WAS와 Mqtt Subscribe로 기기 메시지를 수신한 WAS가 다를 수 있다.
이 경우를 대비하여 공유 저장소를 사용하여 기기 메시지를 수신했을 때 Callback 정보를 확인하는 꼴도 가능하다.
TTL 시간이 명확하고, 장기간 보존이 불필요하기에 공유 저장소로 레디스를 사용하기 좋다.
또는 아예 요청을 수신하고 N초 동안 기기 이벤트를 대기 후, 요청의 응답으로 Callback 하는 꼴도 가능하다.

로그 확인
아래는 구현한 시그널링 서버가 P2P 연결을 맺기까지의 로그이다.
노란색은 FE로부터 WebSocket으로 전달받은 값, 빨간색은 기기로부터 Mqtt로 전달받은 값을 의미한다.
앞선 WebRTC 수립 과정처럼, SDP Offer, Answer와 Ice Candidate를 양쪽에 전달한다.
특히 기기의 ICE Candidate로 로컬 네트워크, Stun 서버로 확인한 외부 IP, Relay 서버를 순서대로 전달하는 것이 재밌다.
그 FE와 기기의 후보군이 오가며, 후보군 중 최적의 경로를 찾게 된다.

이번엔 위 시그널링 서버에서 연결된 프론트엔드 측 로그이다.
시그널링 서버로부터 Stun, Turn 서버를 전달받고, 마찬가지로 SDP Offer, Answer, ICE Candidate가 오가는 것을 볼 수 있다.
WebRTC의 RTCPeerConnection.getStats()를 사용하면, 양 쪽 Peer의 ICE Candidate type을 확인할 수 있다.
이제 우리는 양 쪽 모두 Stun 서버로 확인한 외부 IP로 NAT 홀 펀칭에 성공했고, P2P 연결이 진행되는 것을 이해할 수 있다.

정리
이슈 :
- 외부 플랫폼에 카메라 기기 WebRTC 적용, 시그널링 서버 개발
- WebRTC 통신 흐름, 내부망 기기끼리 직접 통신하는 원리
정리 :
- Nat 동작 원리, 홀 펀칭을 이용한 P2P 통신
- Nat 연결 정보를 알려주는 Stun 서버
- 홀 펀칭이 불가능한 NAT, 네트워크 환경에선 Turn 서버를 사용한 릴레이 통신
- WebRTC 수립 과정, SDP와 ICE 후보군 전달
- 시그널링 서버의 역할, 사용자 세션, Mqtt 연결 정보 저장과 데이터 송수신 중계

'KimJinHwan > Project' 카테고리의 다른 글
| 데이터 적재 처리량 개선 : 단건 처리에서 배치 처리로 (0) | 2025.10.30 |
|---|---|
| OOM 문제 해결 : 스레드 풀, 요청량이 처리량을 넘어설 때 (0) | 2025.10.09 |
| 프로젝트 경험 소개 : 레거시 서비스 인프라 개선 (0) | 2025.10.06 |
| 논 블록킹 처리 : 스레드 사용 효율 개선 경험과 주의할 점 (0) | 2025.09.04 |
| 캐시 도입 : 이벤트 파이프라인 처리량 높이기 (1) | 2025.08.28 |