P2P, Mesh, SFU, MCU, WebRTC 연결 이론, DataChannel 파일 전송을 10,000자급 공부 노트로 정리한 실시간 네트워크 딥다이브.
P2P를 처음 공부할 때 제일 먼저 버린 문장은 “서버가 없다”였다. 그 말은 설명을 쉽게 시작하게 해 주지만 실제 설계에는 거의 도움이 되지 않았다. 로그인은 누가 확인하는지, 방은 누가 만들고 닫는지, 링크 권한은 어디에 남는지, 결제나 감사 로그는 누가 책임지는지 묻기 시작하면 서버는 여전히 한가운데에 있다. 달라지는 것은 서버의 존재가 아니라 서버가 모든 바이트를 떠안느냐의 문제였다.
직접 연결이라는 말은 보기보다 위험하게 단순하다. 처음에는 두 브라우저가 서로의 IP를 알고 곧장 소켓을 여는 장면을 떠올렸다. 같은 공유기 안에 있으면 맞는 그림처럼 보이기도 한다. 하지만 WebRTC를 따라가다 보면 직접 연결은 “주소를 안다”가 아니라 “여러 후보 경로를 모으고 시험한 뒤 실제 패킷이 오갈 수 있는 길을 고른다”에 가깝다.
P2P를 공부하다 보면 한동안 모든 문제를 직접 연결로 풀고 싶어진다. 서버 대역폭을 줄이고, 사용자끼리 바로 주고받고, 중앙 병목도 피한다는 설명은 꽤 매력적이다. 하지만 제품에 넣을 기능을 고를 때는 그 매력부터 의심해야 했다. P2P가 강한 장면은 분명히 있지만, 약한 장면에서 억지로 쓰면 서버 비용보다 더 비싼 실패 처리와 운영 복잡도가 돌아온다.
WebRTC 예제를 처음 따라 칠 때 막힌 곳은 카메라나 파일 전송이 아니었다. 두 브라우저가 서로 어떤 약속을 주고받아야 하는지, 그 약속을 어디로 보내야 하는지가 먼저 막혔다. WebRTC가 연결과 미디어 처리의 큰 틀을 제공해도 signaling 서버를 대신 만들어 주지는 않는다. 어떤 정보를 어떤 프로토콜로 누구에게 전달할지는 애플리케이션이 정해야 한다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, WebRTC 주변에서 반복되던 작은 끊김이었다.
Mesh를 처음 볼 때는 가장 자연스러운 P2P 구조처럼 보였다. 모든 참여자가 서로 직접 연결되어 각자에게 미디어나 데이터를 보내면 중앙 서버는 가벼워진다. 두 명일 때는 설명도 쉽고 구현도 비교적 직관적이다. 문제는 사람이 한 명씩 늘어날 때마다 연결과 업로드 부담이 조용히 곱해진다는 점이다. Mesh는 소규모에서 단순하지만, 규모가 커질수록 각 클라이언트가 작은 서버처럼 일해야 한다. 아래에서는 Mesh을 중심으로, 깨진 가정과 다시 그은 경계를 순서대로 남긴다.
Topology를 처음 그릴 때는 선이 많을수록 더 분산된 구조라고 생각했다. 모두가 모두에게 직접 연결되면 중앙 서버 부담이 줄고, 덜 연결되면 어딘가에 의존한다고 느꼈다. 그런데 실제 제품 관점에서 보니 질문은 조금 달랐다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, Topology 주변에서 반복되던 작은 끊김이었다.
피어 투 피어 그물은 처음 보면 정직하다. 서버가 미디어를 안 나르고 브라우저끼리 직접 붙는다. 구조가 단순하니 구현도 단순해 보인다. 작은 방 데모에서는 그 정직함이 매력으로 남는다. 화상통화에서 그 단순함은 빨리 부메랑이 된다. 연결은 됐는데 화면이 얼고 연결됨 표시와 실제 통화 체감이 어긋난다. 이 글은 그물이 왜 화상통화에서 특히 취약한지, 어느 순간 연결 성공이 체감 실패로 바뀌는지 정리한다.
n(n-1)/2라는 공식은 칠판에서는 가볍다. n명이 들어오면 n개 노드 중 두 노드를 고르는 조합이 몇 개인지만 세면 된다. 그런데 브라우저 네 개를 실제 방에 붙여 보면 숫자의 표정이 달라진다. 선은 여섯 개뿐인데 offer, answer, candidate, 연결 상태, 재시도 로그가 한꺼번에 움직인다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, Mesh 주변에서 반복되던 작은 끊김이었다. 같은 단어를 쓰더라도 제품 문맥이 다르면 답이 갈렸다. 그 갈림길을 숨기지 않는 쪽으로 썼다.
회의 구조를 정리하던 노트에서 P2P, Mesh, SFU, MCU가 같은 줄에 놓였다. 이름만 보면 네 가지 후보를 놓고 하나를 고르는 문제처럼 보인다. 그런데 실제 설계에서는 질문의 층위가 다르다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, 비교 주변에서 반복되던 작은 끊김이었다. 이 질문은 구현 세부보다 앞에 있어야 한다. 나중에 코드가 커진 뒤 상태 이름을 고치려면 로그, 화면 문구, 고객 지원 문장, 알림 정책을 같이 바꿔야 한다. - [ signaling 서버와 media/payload 경로를…
SFU를 처음 공부할 때 가장 헷갈렸던 단어는 미디어 서버였다. 서버가 가운데 있으니 당연히 모든 영상을 받아 새 화면으로 만들어 준다고 생각했다. 그런데 SFU를 따라가 보니 핵심은 합성이 아니라 선택이었다. SFU를 붙인 뒤에도 제품 질문은 남는다. 어떤 subscriber에게 어떤 layer를 보냈는지, 그 선택이 네트워크 때문인지 화면 크기 때문인지, 서버 region 때문인지 알 수 있어야 한다. 앱 로그에는 방 상태와 사용자 행동이 남고, 미디어 서버 로그에는 publisher, subscriber, bitrate,…
SFU가 Mesh보다 확장에 강하다는 말은 맞다. 다만 이유를 “서버를 쓰니까”로 외우면 핵심이 빠진다. *SFU의 실패는 서버 하나로 단순화되지 않는다. 업로드, 서버 전달, 지역, 품질 선택을 나눠서 봐야 한다. | 구조 | 업로드 책임 | 좋아지는 점 | 새로 생기는 책임 | | --- | --- | --- | --- | | Mesh | 각 참여자가 상대마다 복제 | 서버 미디어 비용이 작고 1:1이 단순하다 | 방이 커질수록 클라이언트 업로드와 CPU가 무거워진다 | | SFU | 서버가 수신자별로 복제 | 참여자 업로드…
Simulcast와 SVC를 처음 봤을 때는 코덱 쪽의 어려운 옵션처럼 느껴졌다. 그런데 SFU를 공부하고 나니 둘은 품질을 누가, 어떤 단위로 고를 수 있느냐의 문제였다. SFU가 수신자마다 다른 품질을 전달하려면 고를 재료가 있어야 한다. 송신자가 한 가지 품질만 보내면 SFU는 그것을 전달하거나 끊는 정도밖에 못 한다. 그래서 SFU 옆에는 자연스럽게 simulcast와 SVC가 따라붙는다. 같은 단어를 쓰더라도 제품 문맥이 다르면 답이 갈렸다. 그 갈림길을 숨기지 않는 쪽으로 썼다.
MCU는 요즘 SFU보다 덜 자주 들린다. 그래서 처음에는 낡은 방식 정도로 생각했다. 그런데 녹화, 방송, 고정 레이아웃, 저성능 단말 지원을 떠올리면 MCU를 그렇게 쉽게 버릴 수 없다. SFU는 미디어를 열어 보지 않고 필요한 곳으로 전달하는 데 가깝고, MCU는 서버가 미디어를 디코딩하고 섞고 다시 인코딩한다. 비싸지만 그만큼 서버가 결과물을 책임질 수 있다. 비싸지만만 있으면 나머지는 따라올 거라고 믿었다.
SFU와 MCU를 비교하면 둘 중 하나를 골라야 할 것처럼 보인다. 나도 처음에는 SFU가 현대적이고 MCU는 예전 방식이라고 단순하게 나눴다. 하지만 실제 제품에서는 그렇게 정리하면 금방 막힌다. SFU는 낮은 지연과 확장에 강하고, MCU는 합성 결과물과 단순한 수신 경험에 강하다. 경쟁이라기보다 서로 다른 위치에 놓는 선택지다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, SFU 주변에서 반복되던 작은 끊김이었다.
WebRTC 연결은 버튼 하나로 열리는 것처럼 보인다. 브라우저 API를 호출하고 상대에게 offer와 answer를 주고받으면 끝날 것 같다. 실제로는 그 사이에 꽤 많은 조건이 숨어 있다. 두 사용자가 모두 인터넷에 접속해 있다는 사실과, 서로에게 패킷을 보낼 수 있다는 사실은 다르다. NAT, 방화벽, 회사망, 모바일망, 브라우저 권한, UDP 차단이 모두 연결을 흔든다.
WebRTC 연결 로그를 처음 오래 들여다봤을 때 가장 헷갈린 단어가 STUN, TURN, ICE였다. 셋 다 연결을 도와주는 것처럼 보였고 실제 문서에서도 거의 붙어 다닌다. 그래서 초반에는 STUN 서버를 중계 서버처럼 말하거나, TURN을 넣었으니 ICE 과정은 대충 넘어가도 된다고 생각했다. 로그에 `host`, `srflx`, `relay` 후보가 같이 찍히기 시작하자 그 착각이 바로 드러났다. 이름이 비슷해서가 아니라 각자 맡은 일이 다른데 내가 한 덩어리로 부르고 있었던 것이다.
NAT traversal이라는 말을 처음 보면 NAT를 뚫는 기술처럼 들린다. 실제로는 그렇게 단단한 약속이 아니다. 사설망 뒤에 있는 두 장치가 서로 닿을 수 있는 후보를 찾고, 포트 매핑을 이용해 보고, 안 되면 릴레이로 돌아가는 시도에 가깝다. 이 차이를 놓치면 "WebRTC를 쓰면 직접 연결이 된다"는 말이 너무 쉽게 나온다. 좋은 네트워크에서는 맞아 보이지만 회사망, 학교망, 호텔 Wi-Fi, 일부 모바일망에서는 바로 깨진다. Traversal만 있으면 나머지는 따라올 거라고 믿었다.
P2P 제품을 만들면서 TURN 비용을 계산한다는 말은 처음엔 이상하게 들린다. 사용자는 피어끼리 직접 보내는 기능을 기대하고, 개발자도 서버 대역폭을 줄이려고 WebRTC를 본다. 그런데 직접 경로가 막히면 TURN 서버가 실제 payload를 대신 받아서 다시 보내야 한다. 그 순간 P2P라는 말만으로는 비용을 설명할 수 없다. TURN은 무료 안전망이 아니라 사용량이 찍히는 릴레이 경로다. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, TURN 주변에서 반복되던 작은 끊김이었다. 아래에서는 TURN을…
DataChannel로 파일을 보낼 수 있다는 말을 처음 들으면 브라우저가 파일 전송 API를 제공하는 것처럼 느껴진다. 실제로는 bytes를 주고받는 통로를 받은 것에 가깝다. 파일 이름, 크기, MIME 타입, chunk 순서, 무결성, 재시도, 저장 위치, 진행률은 DataChannel이 알아서 처리하지 않는다. 이 부분을 애플리케이션 프로토콜로 설계해야 파일 전송 기능이 된다.
DataChannel 옵션에서 `ordered`를 처음 봤을 때는 순서를 지켜 주는 쪽이 당연히 더 안전하다고 생각했다. 메시지가 보낸 순서대로 도착하면 애플리케이션이 덜 헷갈릴 것 같았고, 반대로 unordered는 뭔가를 포기한 설정처럼 보였다. 그런데 실시간 전송을 조금만 오래 들여다보면 이 감각이 바로 흔들린다. 어떤 데이터는 순서가 생명이고, 어떤 데이터는 늦게라도 순서대로 오는 순간 이미 쓸모가 줄어든다. Ordered만 있으면 나머지는 따라올 거라고 믿었다.
`reliable`이라는 단어는 이름만 보면 늘 좋은 선택처럼 들린다. 손실된 메시지를 다시 보내 준다니 안전해 보이고, 실시간 제품에서도 일단 reliable로 두면 문제가 줄 것 같다. 나도 처음에는 그렇게 이해했다. 그런데 DataChannel을 파일, 커서, 진행률, 제어 명령에 나눠 대입해 보니 질문이 달라졌다. 모든 데이터가 늦게라도 반드시 도착해야 할까.
파일 전송을 만들 때 처음 떠오르는 질문은 속도다. 얼마나 빨리 보낼 수 있는지, chunk를 얼마나 크게 잡아야 하는지, send를 얼마나 자주 호출할 수 있는지부터 궁금해진다. 그런데 브라우저 DataChannel로 큰 파일을 보내려면 속도보다 먼저 봐야 할 값이 있다. 상대와 내 런타임이 지금 이 속도를 받을 수 있는가. 로그를 다시 펼쳤을 때 눈에 들어온 건 예쁜 성공 화면이 아니라, Backpressure 주변에서 반복되던 작은 끊김이었다.