OpenAI가 WebRTC로 저지연 음성 AI를 구현한 방법: 릴레이+트랜시버 아키텍처 분석 (openai.com)
목차(4)
한줄 요약
OpenAI는 Kubernetes 환경의 WebRTC 포트 고갈 문제를 릴레이+트랜시버 분리 아키텍처로 해결했다.
어떤 상황에서 필요한가?
WebRTC 기반 실시간 음성 서비스를 Kubernetes 위에서 운영하면 반드시 벽에 부딪히는 지점이 있다. 세션마다 별도의 UDP 포트를 열어야 하는 전통적인 WebRTC 모델이 클라우드 인프라와 근본적으로 충돌하기 때문이다.
OpenAI의 경우 주간 활성 사용자 9억 명 이상을 대상으로 ChatGPT 음성, Realtime API, 에이전트 워크플로우를 동시에 운영해야 했다. 여기서 세 가지 요구사항이 동시에 충돌했다. 첫째, 글로벌 사용자에게 고르게 낮은 레이턴시를 보장해야 한다. 둘째, 세션 시작 직후 바로 음성 입력이 가능해야 한다. 셋째, 미디어 라운드트립 타임이 낮고 안정적이어야 자연스러운 대화 턴테이킹이 가능하다.
웹이나 앱에서 WebRTC 기반 음성 기능을 구현해본 개발자라면 비슷한 고민을 해봤을 것이다. 세션 수가 수십을 넘어가는 순간부터 포트 관리, 로드밸런서 설정, 파드 재스케줄링 시 세션 유실 등 문제가 쌓이기 시작한다. OpenAI가 이 문제를 어떻게 해결했는지는 그래서 실무적으로 가치 있는 레퍼런스다.
핵심 구현 방법
OpenAI의 해법은 릴레이(Relay)와 트랜시버(Transceiver)를 명확하게 분리하는 것이다. 처음 구현은 단일 Go 서비스로 Pion 위에서 시그널링과 미디어 터미네이션을 함께 처리했으나, 이 방식은 Kubernetes 환경에서 다음 두 가지 문제를 피할 수 없었다.
포트 고갈 문제부터 보면, 세션당 하나의 UDP 포트를 사용하는 모델은 동시 세션이 많아질수록 수만 개의 공개 UDP 포트 범위를 노출해야 한다. 클라우드 로드밸런서는 이런 구조에 최적화되어 있지 않고, 방화벽 정책 관리도 복잡해지며, 파드가 추가·삭제·재스케줄링되는 Kubernetes 환경에서는 안정적인 포트 범위 유지 자체가 취약해진다.
세션 스티키니스 문제도 있다. ICE와 DTLS는 상태를 가지는(stateful) 프로토콜이다. 특정 세션의 패킷은 반드시 그 세션을 생성한 프로세스로 도달해야 한다. 패킷이 다른 프로세스에 떨어지면 DTLS 핸드셰이크가 실패하거나 SRTP 복호화가 깨진다.
이를 해결하기 위해 설계한 구조가 릴레이+트랜시버 분리 아키텍처다. 핵심 아이디어는 간단하다.
- 릴레이: 인터넷에 노출되는 경량 UDP 포워딩 레이어. 미디어를 복호화하지 않고, ICE 상태 머신도 돌리지 않는다. 패킷 메타데이터만 읽어 목적지를 결정하고 전달한다.
- 트랜시버: 실제 WebRTC 세션 상태를 소유하는 내부 서비스. ICE 연결 검증, DTLS 핸드셰이크, SRTP 키 관리, 세션 라이프사이클 전체를 담당한다.
클라이언트 입장에서는 아무것도 바뀌지 않는다. 표준 WebRTC 플로우 그대로다. 변화는 OpenAI 인프라 내부에서만 일어난다.
첫 번째 패킷 라우팅이 이 구조의 핵심이다. 릴레이는 세션 정보가 없는 상태에서도 첫 패킷을 올바른 트랜시버로 보내야 한다. 여기서 활용하는 것이 ICE 프로토콜에 이미 존재하는 ufrag(username fragment)다. OpenAI는 서버 측 ufrag를 생성할 때 목적지 클러스터와 소유 트랜시버를 추론할 수 있는 라우팅 힌트를 인코딩한다. 릴레이는 STUN 패킷에서 ufrag를 파싱해 외부 조회 없이 즉시 목적지를 결정한다.
SDP 응답에는 릴레이 플릿 전체를 앞단에 두는 단일 VIP와 UDP 포트가 담긴다. 클라이언트는 203.0.113.10:3478 같은 하나의 안정적인 주소만 알면 된다. 각 트랜시버는 내부적으로 세션별 소켓이 아닌 공유 UDP 소켓 하나를 사용하여 다수의 세션을 역다중화한다.
글로벌 레이턴시 최적화를 위해서는 지리적으로 가까운 릴레이로 트래픽을 유도하는 지오-스티어드 시그널링도 함께 운용한다. 첫 번째 홉의 물리적 거리를 최소화하는 것이 전체 레이턴시에서 차지하는 비중이 크기 때문이다.
실전에서 주의할 점
이 아키텍처를 직접 구현하거나 참고해 설계할 때 고려해야 할 현실적인 지점들이 있다.
ufrag 기반 라우팅은 표준 활용이지만 구현 난이도가 있다. ufrag는 WebRTC 표준에 정의된 필드지만, 서버 측에서 라우팅 정보를 안전하게 인코딩하고 릴레이에서 빠르게 파싱하는 로직은 직접 설계해야 한다. 인코딩 방식이 예측 가능해지면 보안 이슈로 이어질 수 있어 신중한 설계가 필요하다.
릴레이가 단일 장애점이 되지 않도록 해야 한다. VIP 뒤에 릴레이 플릿을 두는 구조상, 릴레이 계층의 고가용성 설계가 전체 서비스 가용성을 결정한다. 릴레이 자체는 무상태(stateless)이므로 수평 확장은 쉽지만, 릴레이→트랜시버 구간의 네트워크 경로 안정성도 함께 관리해야 한다.
기존 TURN 서버와의 비교를 명확히 해야 한다. TURN 프로토콜도 릴레이 역할을 하지만, TURN allocation은 설정 라운드트립이 추가되고 서버 간 이전이 어렵다. OpenAI의 릴레이는 TURN이 아닌 커스텀 포워딩 레이어로, 프로토콜 터미네이션 없이 순수하게 전달만 한다는 점이 다르다. 기존 인프라에 TURN을 그대로 쓸 것인지 커스텀 릴레이를 구축할 것인지는 트래픽 규모와 운영 복잡도를 고려해 결정해야 한다.
소규모 서비스라면 이 아키텍처가 오버엔지니어링일 수 있다. 세션이 수백~수천 수준이라면 서버당 하나의 UDP 포트 + 애플리케이션 레벨 역다중화만으로도 충분하다. 이 분리 아키텍처의 진가는 Kubernetes에서 동적으로 수십만 세션을 운용할 때 드러난다.
자주 묻는 질문
Q.WebRTC를 Kubernetes에서 운영할 때 가장 먼저 만나는 문제는 무엇인가?
세션마다 별도의 UDP 포트를 필요로 하는 전통적인 WebRTC 모델이 Kubernetes의 동적 파드 라이프사이클과 충돌하는 것이 핵심이다. 파드가 재스케줄링될 때 기존에 예약한 UDP 포트 범위를 유지하기 어렵고, 클라우드 로드밸런서는 수만 개의 UDP 포트 범위를 처리하도록 설계되어 있지 않다. 결국 오토스케일링이 불안정해지고, 포트 고갈 또는 세션 유실로 이어진다.
Q.ICE ufrag를 라우팅 키로 사용하는 방식이 표준을 위반하는 것 아닌가?
ufrag 자체는 WebRTC 표준(RFC 5245)에 정의된 필드이며, 서버 측에서 값을 생성할 때 특정 정보를 인코딩하는 것은 표준 위반이 아니다. 클라이언트는 ufrag 값을 그대로 STUN 메시지에 실어 보내기만 하면 되고, 그 의미를 해석할 필요가 없다. 다만 인코딩된 정보가 외부에서 추론 가능하다면 보안 위협이 될 수 있으므로, 실제 구현에서는 암호화나 서명을 포함하는 것이 권장된다.
Q.이 아키텍처를 소규모 스타트업이 참고해 직접 구현할 수 있나?
개념적으로는 참고할 수 있지만, 직접 구현하는 것은 상당한 공수가 든다. Pion 같은 오픈소스 WebRTC 라이브러리를 활용하면 트랜시버 부분은 상대적으로 접근하기 쉽다. 릴레이와 트랜시버 간 라우팅 로직, ufrag 인코딩 설계, VIP 기반 플릿 운용은 별도 설계가 필요하다. 세션 규모가 수천 이하라면 단일 서비스에 서버당 단일 UDP 포트 방식으로 시작하고, 실제 규모 문제가 생겼을 때 분리하는 전략이 현실적이다.
관련 아티클
OpenAI가 선택한 저지연 음성 AI 아키텍처: WebRTC 릴레이+트랜시버 구조 분석
가이드Codex CLI /goal 기능으로 AI 에이전트 루프 자동화하기 — 반복 실행 기반 목표 달성 구현 가이
가이드Claude Code UX에 DeepSeek 뇌를 이식하기 — deepclaude 실전 분석
가이드AI 에이전트 시대, IT 에이전시가 클라이언트 프로젝트를 다루는 방식이 달라져야 하는 이유
가이드AI 에이전트 시대, IT 에이전시가 클라이언트 프로젝트에서 놓치고 있는 세 가지
가이드Claude와 Codex를 한 레포에서 같이 쓰는 법: 두 AI 에이전트 역할 분담 워크플로우
관련 사례
이 글의 키워드와 맞닿은 실제 개발 사례를 함께 보세요.