Voice-Link 통화 품질은 왜 WebRTC만으로 끝나지 않는가: TURN, LiveKit, Redis 타임라인, SSE까지 이어지는 실시간 음성 아키텍처

서론

Voice-Link 같은 실시간 음성 서비스에서 가장 흔한 오해는 “WebRTC를 붙였으니 통화 기능은 끝났다”는 생각입니다. 실제 서비스는 그보다 훨씬 복잡합니다. 사용자는 사내망, LTE, 공용 와이파이, VPN, 저사양 단말, 백그라운드 전환, 일시적 네트워크 단절 같은 변수 속에서 통화를 시도합니다. 이때 필요한 것은 단순한 미디어 송수신 라이브러리가 아니라, 연결 성립, 끊김 복구, 통화 종료 판정, 품질 관측, 장애 재현 가능성까지 포함한 운영형 아키텍처입니다.

Voice-Link 코드베이스를 보면 이 방향이 비교적 분명합니다. `application.yml`에는 `livekit.url`, `livekit.api-url`, `app.call-quality.*`, `spring.data.redis.*`, `app.match.*` 같은 설정이 존재하고, 서버 측에는 `LiveKitWebhookService`, `CallQualityService`, `CallEventStreamingService`가 분리되어 있습니다. 이것은 Voice-Link가 단순히 음성 채널 하나를 여는 구조가 아니라, 실시간 미디어 시스템과 Spring Boot 백엔드가 서로 상태를 교환하며 통화 수명주기를 관리하는 구조라는 뜻입니다.

문제상황

실시간 음성 서비스에서 실제 문제는 대개 다음 순서로 발생합니다.

  • 시그널링은 성공했는데 사용자가 서로 목소리를 듣지 못한다.
  • 처음에는 연결되지만 몇 초 뒤 한 명이 이탈한 것처럼 보인다.
  • 통화는 끝났는데 앱 화면은 종료 상태를 제때 반영하지 못한다.
  • 사용자는 “통화가 자꾸 끊긴다”고 말하지만 운영자는 어느 구간에서 왜 끊겼는지 데이터가 없다.
  • 재접속은 일어났는데, 그것이 일시적 품질 저하였는지 완전한 장애였는지 구분되지 않는다.

이 문제들은 전부 하나의 기술로 해결되지 않습니다. NAT 환경을 통과하지 못하면 TURN이 필요하고, 미디어 서버 상태와 백엔드 세션 상태가 어긋나면 웹훅 정리가 필요하며, 장애 원인을 남기려면 품질 스냅샷과 타임라인 저장이 필요합니다. 즉 Voice-Link에서 중요한 것은 “통화를 붙이는 기술”보다 통화의 생애주기를 끝까지 추적하는 기술입니다.

Voice-Link와의 연결: 왜 이 기술들이 필요한가

1. WebRTC와 VoIP는 Voice-Link의 핵심이지만, 운영 포인트는 연결 이후에 생긴다

Voice-Link의 음성 통화는 WebRTC 기반 실시간 전송 구조로 이해하는 것이 자연스럽습니다. `LiveKitTokenService`를 보면 서버가 세션 단위 토큰을 발급하고, 사용자 식별값을 subject로 넣고, 특정 room에 join/publish/subscribe가 가능하도록 권한을 부여합니다. 이는 단순 로그인 토큰이 아니라 실시간 미디어 세션에 참여시키기 위한 VoIP 세션 토큰에 가깝습니다.

중요한 점은 여기서 끝나지 않는다는 것입니다. 통화 서비스에서 진짜 문제는 토큰 발급이 아니라, 발급된 뒤 사용자가 다양한 네트워크 조건에서 실제로 음성을 주고받을 수 있느냐입니다. Voice-Link에서 이 기술이 필요한 이유는 분명합니다. 사용자가 매칭 직후 즉시 음성 대화를 시작해야 하므로, 지연이 길거나 연결 실패율이 높으면 서비스 경험 자체가 무너집니다.

2. TURN / STUN / NAT traversal은 “왜 어떤 사용자는 통화가 되고 어떤 사용자는 안 되는가”를 설명한다

`docs/turn-stun-flow.md`는 Voice-Link에서 STUN, TURN, ICE가 어떤 역할을 하는지 정리하고 있습니다. 이 문서 기준으로 보면 구조는 전형적이지만, 운영상 의미는 매우 실무적입니다.

  • STUN은 클라이언트가 자신의 외부 후보 주소를 파악하게 돕습니다.
  • TURN은 직접 경로가 막힐 때 미디어를 릴레이합니다.
  • ICE는 여러 후보 중 실제로 연결 가능한 경로를 선택합니다.

이 조합이 필요한 이유는 Voice-Link 사용자가 항상 같은 네트워크에 있지 않기 때문입니다. 집 와이파이에서는 바로 붙는 통화가 기업망이나 이동통신망에서는 실패할 수 있습니다. 특히 음성 서비스는 텍스트 채팅과 달리 “조금 느림”이 아니라 “통화 불가”로 체감되므로, TURN은 선택 기능이 아니라 연결 생존율을 보장하는 장치에 가깝습니다.

리포지토리의 TURN 문서와 coturn 설정을 보면 외부 TURN 구성을 따로 두는 형태이며, TLS 기반 경로를 함께 고려하고 있습니다. 여기서 운영 포인트는 명확합니다. 시그널링 성공률과 실제 음성 송수신 성공률은 별개이므로, “웹소켓 붙음”만 보고 통화 성공으로 판단하면 안 됩니다.

3. LiveKit 같은 실시간 미디어 서버는 단순 중계기가 아니라 상태 기준점이다

Voice-Link 코드베이스에는 LiveKit 관련 설정과 서비스가 분명히 존재합니다. `livekit.url`, `livekit.api-url` 설정이 있고, `LiveKitRoomStateService`는 REST API를 통해 참여자 수를 확인하며, `LiveKitWebhookService`는 `room_finished`, `participant_left`, `participant_disconnected`, `participant_joined` 이벤트를 처리합니다.

이 구조는 Voice-Link에서 실시간 미디어 서버가 왜 필요한지를 잘 보여줍니다. 음성 서비스에서 서버는 단순히 패킷을 통과시키는 장비가 아니라, 현재 방에 누가 남아 있는지, 통화가 정말 끝났는지, 일시적 네트워크 이탈인지를 판별하는 기준점이 됩니다.

특히 `LiveKitWebhookService`에는 참가자 이탈 후 즉시 종료하지 않고 짧은 유예 시간을 둔 뒤, `RtcRoomStatePort.hasInsufficientParticipants(roomName)`로 실제 참여자 부족 상태를 다시 확인하는 로직이 있습니다. 이것은 매우 중요합니다. 모바일 네트워크에서는 일시적 단절 뒤 재입장이 흔하기 때문에, 이 유예가 없으면 사용자는 잠깐 끊겼다가 돌아왔는데 서버는 이미 통화를 종료해 버리는 문제가 생깁니다.

4. Redis 기반 통화 품질 타임라인은 “느낌”을 “데이터”로 바꾸는 장치다

`CallQualityService`는 Voice-Link 운영 관점에서 가장 중요한 구성요소 중 하나입니다. 이 서비스는 세션별 품질 스냅샷을 Redis 리스트에 저장하고, 최대 개수를 제한하고, 보관 기간을 두고, 특정 조건에서는 진단 리포트를 발생시킵니다.

이 구조가 필요한 이유는 단순합니다. 통화 품질 문제는 대부분 사후 분석이 필요하기 때문입니다. 사용자는 “끊겼어요”, “목소리가 로봇처럼 들렸어요”, “다시 붙긴 했어요”라고 말하지만, 운영자는 그 순간의 RTT, jitter, packet loss, reconnect 횟수, TURN relay 사용 여부를 알아야 원인을 분류할 수 있습니다.

`CallQualitySnapshotRequest`를 보면 Voice-Link가 수집하려는 값은 꽤 실무적입니다.

  • `jitterMs`
  • `packetLossRatio`
  • `roundTripTimeMs`
  • `connectionState`
  • `disconnectReason`
  • `reconnectCount`
  • publisher/subscriber 측 candidate type
  • transport 정보

이 값들은 “좋다/나쁘다” 수준의 감상이 아니라, 어디서 품질이 무너졌는지 추적하는 최소 관측 단위입니다. 예를 들어 RTT만 높고 packet loss는 낮다면 단순 지연 문제일 가능성이 있고, jitter와 packet loss가 함께 치솟고 relay 사용 흔적이 보인다면 불안정한 네트워크와 TURN 의존 구간을 의심할 수 있습니다.

5. SSE는 미디어 전송 기술이 아니지만, 통화 경험 완성에 필수다

`CallEventStreamingService`는 음성 데이터를 보내는 컴포넌트가 아닙니다. 대신 통화 종료와 연장 같은 이벤트를 SSE로 앱에 전달합니다. 20초 heartbeat를 두고 emitter를 관리하는 구조도 포함되어 있습니다.

이 기술이 Voice-Link에서 왜 필요한가를 생각해보면 답은 간단합니다. 음성 패킷은 WebRTC로 가더라도, 통화 상태 UI는 별도의 애플리케이션 이벤트 채널이 필요하기 때문입니다. 사용자가 통화 종료를 누르거나, 상대가 나갔거나, 서버가 세션을 정리했을 때 앱 화면이 이를 즉시 반영해야 혼란이 줄어듭니다.

즉 Voice-Link에서 실시간 음성은 WebRTC만으로 구성되지 않습니다. 미디어는 WebRTC가 맡고, 상태 전파는 SSE가 맡고, 세션 정리는 Spring 서비스와 웹훅이 맡는 식으로 역할을 분리해야 안정성이 높아집니다.

구현방법

1. 통화 시작: Spring Boot가 미디어 세션 입구를 통제한다

Voice-Link 구조를 보면 백엔드는 단순 인증 서버가 아니라 실시간 음성 세션의 진입점을 담당합니다. `LiveKitTokenService`는 세션 ID와 사용자 정보를 기반으로 미디어 서버 접속 토큰을 생성합니다. 여기서 중요한 구현 포인트는 다음과 같습니다.

  • 토큰은 세션 단위로 발급되어야 한다.
  • 사용자 식별값은 서버 도메인 모델과 일치해야 한다.
  • publish/subscribe 권한은 최소한으로 명시적으로 부여해야 한다.
  • 만료 시간은 너무 짧지도 길지도 않게 운영 정책에 맞춰야 한다.

Voice-Link에서 이 기술을 어떻게 적용할 수 있는지 구체적으로 말하면, 매칭이 성사된 뒤 백엔드가 두 참가자 각각에게 동일한 room 기준 토큰과 RTC 접속 URL을 내려주고, 앱은 이를 이용해 LiveKit 방에 붙게 만들 수 있습니다. 이렇게 해야 통화 권한과 세션 식별이 Spring 도메인 모델과 분리되지 않습니다.

2. 통화 유지: TURN 경로를 “예외 처리”가 아니라 “기본 운영 요소”로 봐야 한다

`docs/turn-stun-flow.md`와 coturn 설정 파일이 동시에 존재한다는 것은 Voice-Link가 NAT traversal을 실제 운영 문제로 다루고 있다는 뜻입니다. 여기서 구현 포인트는 TURN을 단지 비상용으로 두는 것이 아니라, 다음을 기준으로 설계하는 것입니다.

  • 직접 경로가 실패하는 환경을 전제로 테스트할 것
  • TLS 기반 TURN 경로를 함께 고려할 것
  • 릴레이 사용 여부를 품질 데이터에 남길 것
  • TURN 서버 장애 시 통화 실패율이 어떻게 변하는지 별도 관측할 것

실무에서는 TURN을 붙였는지보다, TURN을 타는 세션이 어느 정도인지, TURN을 타는 세션에서 jitter와 loss가 더 높은지, TLS relay 사용이 특정 네트워크에서 생존율을 끌어올리는지를 봐야 합니다. Voice-Link의 품질 타임라인 구조는 이런 분석의 기반이 됩니다.

3. 종료 판정: 웹훅 이벤트를 그대로 믿지 말고 재확인해야 한다

`LiveKitWebhookController`는 인증 헤더가 없는 요청을 거부하고, 유효한 웹훅만 `LiveKitWebhookService`로 넘깁니다. 그 뒤 서비스는 이벤트 종류별로 처리합니다. 하지만 더 중요한 것은 처리 방식입니다.

  • `room_finished`는 비교적 강한 종료 신호다.
  • `participant_left`나 `participant_disconnected`는 즉시 종료 신호로 보기 어렵다.
  • `participant_joined`가 다시 들어오면 보류 중이던 종료를 취소해야 한다.
  • 최종 종료 전에 실제 방 참여자 수를 재확인해야 한다.

이 설계는 Voice-Link 같은 서비스에서 매우 현실적입니다. 사용자가 지하철 이동 중 네트워크가 순간적으로 바뀌면, 미디어 서버 이벤트만 보면 “나갔다”로 보이지만 실제 사용자 경험은 “잠깐 끊겼다가 다시 붙음”일 수 있습니다. 운영 포인트는 웹훅을 절대적 진실로 처리하지 않고, 시간 유예 + 현재 방 상태 재확인 + 세션 종료 확정의 3단계로 가는 것입니다.

4. 품질 진단: 타임라인을 저장하고 요약 규칙을 둬야 한다

`CallQualitySessionSummaryResponse`를 보면 Voice-Link는 단순 raw 데이터 저장에서 멈추지 않고, 세션 단위 요약과 진단 라벨까지 계산합니다. 예를 들어 다음 같은 분류가 가능합니다.

  • `RECONNECT_EXHAUSTED`
  • `RECOVERED_AFTER_RECONNECT`
  • `NETWORK_SEVERELY_DEGRADED`
  • `NETWORK_UNSTABLE`
  • `TURN_RELAY_USED`
  • `TURN_RELAY_NOT_OBSERVED`

이 방식의 장점은 장애 대응 속도입니다. 운영자는 원시 지표 수십 개를 일일이 읽지 않고도 “재접속 끝에 결국 실패한 세션”, “끊기진 않았지만 품질이 크게 흔들린 세션”, “TURN relay를 사용한 세션”을 우선순위 있게 볼 수 있습니다.

또한 `CallQualityDiagnosisReporter`는 특정 진단 결과를 로그와 Sentry에 남깁니다. 이것은 단순 모니터링이 아니라 품질 이벤트를 운영 경보 체계에 연결하는 설계입니다. 실시간 음성 서비스에서는 CPU나 메모리보다, 실제 통화 실패 징후를 더 빨리 잡는 경보가 더 중요할 때가 많습니다.

5. 실시간 음성 스트리밍 이후 기능: STT, VAD, 자막, 이벤트 처리는 “붙이는 기능”이 아니라 파이프라인 설계 문제다

현재 제공된 Voice-Link 코드 포인트만 보면 STT나 VAD 파이프라인이 본격 구현되어 있다고 단정할 근거는 없습니다. 따라서 없는 구조를 지어내면 안 됩니다. 다만 아키텍처 관점에서는 다음이 중요합니다.

STT, VAD, 녹취, 자막, 음성 이벤트 처리는 모두 “오디오를 어디서 어떻게 뽑아 후처리할 것인가”의 문제입니다. 이 기능들은 WebRTC 통화가 붙었다는 사실만으로 자동 완성되지 않습니다. 미디어 서버, 별도 오디오 처리 워커, 저장 정책, 개인정보 처리 기준, 실패 시 재처리 정책이 필요합니다.

Voice-Link에 이 기술을 적용한다면, 우선순위는 STT 모델 선택보다 품질 좋은 입력 오디오를 안정적으로 확보할 수 있느냐입니다. TURN 의존 구간이 많고 packet loss가 높은데 바로 자막 정확도를 논하면 순서가 거꾸로 됩니다. 즉 STT/VAD는 음성 서비스의 확장 기능이지만, 그 토대는 여전히 WebRTC 품질 관측과 네트워크 안정화입니다.

주의점

1. “통화 종료”와 “일시적 연결 이탈”을 혼동하지 말 것

Voice-Link 코드에서 참가자 이탈 이벤트에 유예 시간을 둔 이유가 바로 이것입니다. 사용자의 네트워크 전환, 앱 백그라운드 진입, 단말 자원 문제는 실제 종료와 다르게 다뤄야 합니다. 종료를 너무 빨리 확정하면 재입장 가능한 통화를 불필요하게 끊게 됩니다.

2. TURN 사용 여부를 남기지 않으면 장애 분석이 반쯤 막힌다

통화가 왜 느렸는지, 왜 특정 환경에서만 실패했는지 설명하려면 relay 사용 여부와 candidate type이 필요합니다. Voice-Link의 품질 스냅샷 필드에 candidate type이 들어가는 이유가 여기에 있습니다. 이것이 없으면 네트워크 장애와 애플리케이션 버그를 구분하기 어려워집니다.

3. 품질 수집은 “저장”보다 “요약 규칙”이 중요하다

지표를 많이 모아도 운영자가 이해하지 못하면 의미가 없습니다. Voice-Link처럼 reconnect, jitter, packet loss, relay 사용 여부를 조합해 요약 라벨을 만드는 접근은 실용적입니다. 특히 고객 문의 대응이나 장애 회고에서는 원시 로그보다 세션 요약이 훨씬 유용합니다.

4. SSE 채널도 운영 대상이다

음성 패킷은 정상인데 앱 UI가 통화 종료를 늦게 반영하면 사용자는 전체 시스템이 고장 났다고 느낍니다. `CallEventStreamingService`의 heartbeat는 이런 문제를 줄이기 위한 기본 장치입니다. SSE 연결 수, heartbeat 실패율, emitter 정리 누락 여부도 함께 봐야 합니다.

5. STT/녹취를 붙일 때는 기능보다 보안과 저장 정책을 먼저 정해야 한다

음성 인식, 자막, 이벤트 추출은 분명 Voice-Link 확장 가치가 있습니다. 하지만 음성 데이터는 텍스트보다 민감합니다. 따라서 저장 기간, 접근 권한, 마스킹 범위, 후처리 실패 시 폐기 정책이 먼저 정리되어야 합니다. 특히 운영 문서나 블로그 글에서는 실제 인증값, 서버 식별값, 내부 엔드포인트를 그대로 드러내면 안 됩니다.

마무리

Voice-Link에서 중요한 기술은 “WebRTC를 쓴다”는 한 문장으로 설명되지 않습니다. 실제로는 LiveKit 같은 미디어 서버, TURN/STUN 기반 NAT traversal, Redis에 남는 통화 품질 타임라인, Spring Boot의 웹훅 처리, SSE를 통한 상태 전파가 서로 맞물려야 서비스가 완성됩니다.

정리하면, Voice-Link의 실시간 음성 기술에서 핵심은 멋진 기능을 많이 붙이는 것이 아니라 통화 연결 성공률을 높이고, 흔들리는 세션을 복구하고, 실패 원인을 다시 읽어낼 수 있게 만드는 것입니다. 이 기준으로 보면 WebRTC는 출발점이고, 운영 가능한 음성 시스템을 만드는 일은 그 이후부터 시작됩니다.

2~3문장 요약

Voice-Link에서 WebRTC는 통화의 시작일 뿐이며, 실제 서비스 품질은 TURN 경로 확보, 미디어 서버 상태 관리, 재접속 판정, 품질 타임라인 저장 같은 운영 기술에 의해 결정됩니다. 현재 코드 구조는 LiveKit, Redis, SSE, Spring Boot 웹훅을 통해 이 문제를 실제 서비스 관점에서 다루고 있으며, 향후 STT·자막·음성 이벤트 처리도 이 기반 위에서만 안정적으로 확장할 수 있습니다.

댓글 남기기