WebRTC를 파고들었더니 — ICE, DTLS, SFU까지 화상통화 뒤에서 벌어지는 일
SW마에스트로 프로젝트에서 실시간 기능을 붙여야 했다. 처음엔 그냥 WebSocket으로 해결할 수 있을 거라 생각했다. 그런데 팀원끼리 논의하다 보니 “그럼 영상은요?” 라는 질문이 나왔다. WebSocket으로 영상을 어떻게 처리하지?
그 순간부터 WebRTC를 제대로 파보기 시작했다.
처음에 제일 헷갈렸던 것
WebRTC가 “브라우저끼리 P2P로 직접 연결”이라는 설명은 많이 봤다. 근데 직접 연결이면 서버가 필요 없다는 건데, 왜 Zoom이나 Google Meet은 서버가 있는 거지?
결론부터 말하면 “직접 연결”은 미디어 데이터 전송 경로를 말하는 거고, 연결을 맺는 과정에는 서버가 꽤 많이 관여한다. 이걸 모르고 “P2P라서 서버 없이 된다”고 생각하면 설계부터 틀린다.
WebRTC 연결이 실제로 어떻게 이루어지는지, 단계별로 짚어봤다.
연결 전에 벌어지는 일들 — SDP와 ICE
SDP: “나 이런 스펙이야”를 교환하는 문서
두 브라우저가 통화를 시작하기 전에 서로를 소개해야 한다. “나는 VP8 코덱 쓸 수 있어”, “오디오는 Opus로 보낼게”, “내 비트레이트는 최대 이 정도야” 같은 정보들이다.
이걸 담은 게 SDP(Session Description Protocol)다. 텍스트 형식 문서인데, 실제로 열어보면 이런 식이다.
1
2
3
4
5
6
v=0
o=- 46117678 2 IN IP4 127.0.0.1
m=video 9 UDP/TLS/RTP/SAVPF 96 97
a=rtpmap:96 VP8/90000
a=rtpmap:97 H264/90000
a=fingerprint:sha-256 AB:CD:12:34:... ← 보안 지문 (나중에 나옴)
SDP는 WebRTC가 직접 전송하지 않는다. 시그널링 서버라는 중개자를 통해 교환한다. WebSocket이나 HTTP로 만든 그냥 메시지 중계 서버다. WebRTC 표준에서 시그널링 방법을 아예 규정하지 않아서 개발자가 직접 구현해야 한다.
ICE: 어떤 경로로 연결할지 협상
SDP 교환이 끝나면 실제로 패킷이 오갈 경로를 정해야 한다. 브라우저끼리 직접 연결하려면 상대방 IP와 포트를 알아야 하는데, 여기서 문제가 생긴다.
대부분의 기기는 공유기(NAT) 뒤에 있다. 내 컴퓨터의 IP는 192.168.0.5 같은 사설 IP고, 외부에서 보이는 IP는 다르다. 상대방이 192.168.0.5로 패킷을 보내봤자 도달하지 않는다.
ICE(Interactive Connectivity Establishment)가 이 문제를 해결한다.
연결 가능한 경로 후보들을 모아서 하나씩 시도해보는 방식이다.
1
2
3
후보 1: 사설 IP 직접 연결 (같은 네트워크라면 성공)
후보 2: 공인 IP 연결 (STUN 서버로 확인)
후보 3: TURN 서버를 통한 중계 (방화벽 뚫리는 마지막 수단)
STUN은 “내가 외부에서 어떤 IP로 보이는지 알려주는 서버”다. 구글이 공짜로 운영하는 stun.l.google.com:19302를 많이 쓴다. 15ms도 안 걸리고, 미디어 데이터는 거치지 않으니 부하도 없다.
TURN은 P2P가 정말 안 될 때 미디어까지 중계해주는 서버다. 기업 방화벽이 UDP를 막아버리는 환경에서 필요하다. 미디어가 서버를 거치니까 비용이 생긴다. 실제 서비스에서 TURN 트래픽이 전체의 15~20% 정도 된다고 알려져 있다.
ICE 협상이 끝나면 드디어 두 브라우저 사이에 UDP 경로가 하나 생긴다. 근데 이제부터가 진짜다.
연결 직후 — DTLS로 열쇠를 교환한다
ICE로 경로를 뚫었다고 바로 영상을 보내면 안 된다. WebRTC 표준은 모든 미디어를 암호화하도록 강제한다. 평문 RTP는 사용 금지다.
암호화를 하려면 열쇠가 필요하다. 이 열쇠를 안전하게 교환하는 게 DTLS 역할이다.
DTLS는 TLS를 UDP에서 동작하도록 변형한 것이다. 동작 방식은 HTTPS에서 쓰는 TLS 핸드셰이크와 비슷하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
ICE 경로 확정
│
▼
DTLS 핸드셰이크
A: "DTLS 1.2 쓸게, 내 인증서야"
B: 인증서 Fingerprint 검증 ← SDP에 미리 적어둔 값과 비교
B: "확인됐어, 이건 내 인증서"
A: 검증 완료
──────────────────────
양쪽이 동일한 SRTP 마스터 키 도출
│
▼
미디어 전송 시작
여기서 중요한 게 Fingerprint 검증이다. DTLS 핸드셰이크 중에 인증서를 주고받는데, 이게 진짜인지 어떻게 확인하나? 아까 SDP에 a=fingerprint:sha-256 AB:CD:... 라고 미리 적어뒀다. 실제 인증서 해시와 이 값이 일치하면 진짜, 다르면 연결을 끊어버린다. 해커가 중간에서 인증서를 바꿔치기하는 걸 막는 방법이다.
DTLS가 끝나면 양쪽이 같은 열쇠를 갖게 된다. 이제 이 열쇠로 미디어를 잠그면 된다.
미디어 전송 — SRTP가 영상을 잠근다
SRTP(Secure RTP)는 RTP 패킷에 암호화를 추가한 것이다.
RTP 패킷 구조를 보면 이렇다.
1
2
3
4
5
6
7
┌────────────────────────────────┐
│ 헤더 — 번호표, 타임스탬프 등 │ 평문 유지 (라우팅에 필요)
├────────────────────────────────┤
│ 페이로드 — 영상/음성 데이터 │ AES-128로 암호화
├────────────────────────────────┤
│ Auth Tag — HMAC 서명 10바이트 │ 변조 감지용
└────────────────────────────────┘
헤더는 잠그지 않는다. 중간 라우터가 헤더를 보고 어디로 보낼지 결정해야 하기 때문이다. 페이로드만 잠근다. 그리고 Auth Tag가 붙어있어서 누군가 내용을 살짝 바꿔치기하면 서명이 틀려져서 잡힌다.
브라우저에서 WebRTC 쓸 때 이 부분은 자동으로 처리된다. 개발자가 손댈 게 없다. 하지만 서버 측에서 SFU를 직접 구현한다면 SRTP를 알아야 한다.
여러 명이 통화한다면 — SFU vs MCU
1:1이라면 P2P로 충분하다. 근데 4명, 10명이 화상회의를 한다면 어떻게 되나.
단순하게 생각하면 각자가 나머지 전원에게 스트림을 보내면 된다. 4명이라면 태현이는 민준·지수·현수에게 동시에 3개 스트림을 업로드해야 한다. 100명이면 99개. 업로드 대역폭이 폭발한다. 현실적으로 불가능하다.
그래서 서버가 중간에 들어간다. 서버가 어떤 역할을 하느냐에 따라 MCU와 SFU로 나뉜다.
MCU — 서버가 영상을 합쳐서 준다
MCU(Multipoint Control Unit)는 서버가 모든 스트림을 받아서 하나로 합친 뒤 각자에게 보내는 방식이다.
각 참가자는 스트림 하나만 받으면 되니까 단말 부담이 없다. 하지만 서버가 디코딩 → 합성 → 재인코딩을 다 해야 한다. CPU를 많이 쓴다. 참가자가 늘어날수록 서버 비용이 급격히 오른다. 화면 레이아웃도 서버가 정해버리니까 “저 사람 화면만 크게 보고 싶다”가 안 된다.
지금은 PSTN(일반 전화망) 연동이나 녹화 파일 합성 같은 특수 상황에서 쓰인다.
SFU — 서버는 배달부, 편집은 안 한다
SFU(Selective Forwarding Unit)는 서버가 스트림을 받아서 각자에게 그대로 전달하는 방식이다. 디코딩을 하지 않는다. 패킷을 열어보지 않고 복사해서 나눠준다.
서버 부하가 훨씬 적다. 대역폭은 여전히 필요하지만 CPU는 아낀다. 그리고 받는 사람이 직접 화면 배치를 정할 수 있다. “민준이 화면만 크게” 같은 게 가능하다.
현재 Google Meet, Discord, Jitsi, Zoom(웹 버전)이 모두 SFU 기반이다. 오픈소스로는 mediasoup, LiveKit, Janus가 있다.
Simulcast라는 기법도 SFU랑 같이 쓰인다. 송신자가 같은 영상을 고/중/저 3가지 화질로 동시에 보내면, SFU가 수신자 네트워크 상태에 따라 적당한 화질을 골라서 전달한다.
1
2
3
4
5
6
7
8
9
10
// 브라우저에서 Simulcast 설정
const sender = pc.addTrack(videoTrack, stream);
sender.setParameters({
encodings: [
{ rid: 'high', maxBitrate: 2_500_000 }, // 고화질
{ rid: 'mid', maxBitrate: 700_000 }, // 중화질
{ rid: 'low', maxBitrate: 150_000 }, // 저화질
]
});
// SFU가 수신자별로 레이어를 선택해서 전달
전체 흐름 정리
공부하면서 가장 도움됐던 게 이 흐름이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. SDP 교환 (시그널링 서버 통해)
→ 서로 "이런 코덱, 이런 스펙이야" 알려줌
2. ICE 협상
→ 연결 가능한 경로 후보들 수집 (STUN으로 공인 IP 파악)
→ 하나씩 시도해서 성공한 경로 확정
3. DTLS 핸드셰이크
→ 공개키 암호화로 SRTP 열쇠 교환
→ SDP Fingerprint로 상대방 인증
4. SRTP 미디어 전송
→ 모든 영상·음성을 AES-128로 암호화해서 전송
5. 다자 통화라면 SFU가 중간에서 선택적 전달
처음에 “P2P라서 서버가 없다”고 생각했는데, 실제로는 시그널링 서버, STUN 서버, 상황에 따라 TURN 서버, 그리고 다자 통화면 SFU까지 여러 서버가 관여한다. P2P는 “미디어 데이터가 가능하면 직접 오간다”는 의미였다.
마무리
WebRTC가 “그냥 쓰면 되는” 기술이라고 생각했는데, 내부를 파다 보니 ICE, DTLS, SRTP, SFU가 각자 맡은 역할이 명확하게 분리돼 있었다. 레고 조각처럼 하나씩 빠지면 어떤 문제가 생기는지도 이해가 됐다.
SW마에스트로 프로젝트에서 실시간 기능을 어떻게 구현할지 고민 중인데, WebRTC + SFU 조합이 후보에 올라있다. 직접 mediasoup를 붙여보고 싶다. 다음엔 WHIP·WHEP를 공부해볼 예정이다. WebRTC를 인제스트 구간(OBS → 서버)에도 쓰자는 신규 표준이다.



