Voice-Link에서 WebRTC 연결 실패를 줄이는 핵심 기술: TURN/TLS, LiveKit 웹훅, 통화 품질 타임라인을 Spring Boot와 묶는 방법

1. 문제

Voice-Link에서 가장 큰 기술 과제는 “통화 버튼은 눌렸는데 실제 음성이 안정적으로 오가지 않는 상황”을 줄이는 것입니다.
실시간 음성 서비스는 로그인이나 게시판처럼 요청-응답 한 번으로 끝나지 않습니다. 브라우저, 모바일 네트워크, 사내망, NAT 환경, 일시적인 재연결, 상대방 이탈, 서버 이벤트까지 한 세트로 맞물려야 통화가 완성됩니다.

특히 Voice-Link처럼 사람 간 음성 연결이 서비스 핵심인 구조에서는 단순히 WebRTC를 붙였다고 끝나지 않습니다. 시그널링은 붙었는데 미디어가 안 흐르는 경우, 잠깐 끊겼다가 복구되는 경우, 한쪽만 종료로 인식하는 경우를 운영 단계에서 반드시 다뤄야 합니다.

리포지토리 기준으로도 이런 문제의식이 이미 코드에 반영돼 있습니다. `src/main/resources/application.yml`에는 `livekit.url`, `livekit.api-url`, `app.match.*`, `app.call-quality.*`, `spring.data.redis.*`가 분리되어 있고, 이는 통화 연결, 매칭 세션 수명, 품질 기록, 실시간 상태 저장을 각각 독립적으로 운영하려는 구조로 읽힙니다.

또한 `kr.co.voicelink.livekit.LiveKitWebhookService`는 `room_finished`, `participant_left`, `participant_disconnected`, `participant_joined` 이벤트를 구분합니다. 이것은 Voice-Link가 단순히 “방이 열렸다/닫혔다”만 보는 것이 아니라, 실제 사용자 이탈과 재입장을 통화 종료 판단에 반영하려는 서비스라는 뜻입니다.

  • 시그널링은 성공했지만 오디오 경로가 열리지 않는 문제
  • 네트워크 흔들림으로 잠깐 끊겼는데 통화가 즉시 종료 처리되는 문제
  • 품질 저하가 발생했지만 원인 데이터가 남지 않는 문제
  • 통화 종료 사실이 프론트에 늦게 전달되어 UX가 어긋나는 문제

이 문제들은 일반적인 IT 상식으로는 잘 해결되지 않습니다. Voice-Link에는 WebRTC 자체보다도, WebRTC를 운영 가능한 서비스로 만드는 TURN, 품질 측정, 이벤트 스트리밍, 종료 판정 로직이 더 중요합니다.

2. 왜 필요한가

Voice-Link에서 이 기술이 필요한 이유는 “통화 연결 성공률”과 “통화 실패 원인 추적 가능성”이 곧 서비스 신뢰도이기 때문입니다.
사용자는 WebRTC, TURN, ICE를 알 필요가 없습니다. 대신 “왜 이번 통화는 바로 연결되지 않았는가”, “왜 갑자기 종료됐는가”, “문제가 내 네트워크인지 서비스 문제인지”가 분명해야 다시 서비스를 씁니다.

Voice-Link 코드베이스를 보면 이 필요성이 구조적으로 드러납니다. `docs/turn-stun-flow.md`는 STUN이 공인 경로 후보를 찾고, TURN이 직접 연결 실패 시 미디어를 릴레이하며, ICE가 최종 경로를 선택하는 흐름을 정리합니다. 즉 Voice-Link는 애초부터 다양한 NAT 환경을 전제로 연결 전략을 설계하고 있습니다.

여기서 핵심은 TURN의 존재 자체보다 “제한된 네트워크에서도 살아남는 경로를 준비하는 것”입니다. 모바일망, 사내 보안망, 공용 와이파이에서는 UDP 기반 직접 연결이 막히거나 불안정할 수 있습니다. 이런 경우 TURN/TLS 같은 우회 경로가 없으면 사용자는 통화 시도는 했지만 실제 대화는 시작하지 못합니다.

Voice-Link에서 이 기술을 어떻게 적용할 수 있는지 구체적으로 말하면, 매칭이 성사된 뒤 백엔드는 LiveKit 접속 정보와 토큰을 내려주고, 클라이언트는 ICE 후보를 수집해 가장 안정적인 경로를 선택합니다. 이때 직접 연결이 실패해도 TURN 릴레이로 통화를 살려야 하며, 그 이후 품질 지표를 백엔드에 계속 보내야 “연결은 됐지만 품질이 나빴다”는 사실까지 분리해서 볼 수 있습니다.

`kr.co.voicelink.callquality.service.CallQualityService`가 Redis 리스트에 snapshot을 쌓고, 길이를 trim하고, 만료 시간을 두는 이유도 여기에 있습니다. 품질 데이터는 영구 보관보다 “최근 통화에서 무슨 일이 있었는지 빠르게 재구성”하는 데 가치가 더 큽니다. 이 설계는 운영자 입장에서 매우 실용적입니다.

`kr.co.voicelink.match.service.CallEventStreamingService`의 SSE emitter와 주기적 heartbeat 역시 같은 맥락입니다. 통화 종료나 연장 이벤트가 프론트에 늦게 전달되면 사용자는 이미 끝난 통화 화면에 머무르거나, 반대로 아직 대화 가능한 상황에서 종료 UI를 볼 수 있습니다. 즉 미디어 경로와 별개로, 제어 이벤트 경로도 끊기지 않게 관리해야 합니다.

3. 기대효과

이 구조를 제대로 갖추면 Voice-Link는 “통화가 되는 서비스”에서 끝나지 않고, “통화 품질을 운영할 수 있는 서비스”로 올라갑니다.
실시간 음성 서비스의 경쟁력은 기능 목록보다 연결 성공률, 재연결 복원력, 장애 분석 속도에서 더 선명하게 드러납니다.

첫째, TURN/TLS와 NAT traversal 운영 포인트를 명확히 하면 네트워크 환경에 따른 연결 실패율을 줄일 수 있습니다. 사용자는 같은 기능을 써도 어느 네트워크에서는 되고 어느 곳에서는 안 되는 서비스를 가장 불신합니다. TURN은 이 불신을 낮추는 보험 장치입니다.

둘째, LiveKit 웹훅 기반 종료 판정은 “진짜 종료”와 “잠깐 끊김”을 구분하는 데 효과적입니다. `LiveKitWebhookService`에 참가자 이탈 후 즉시 종료하지 않고 유예 시간을 두는 로직이 있는 것은, 네트워크 변동을 서비스 종료로 오판하지 않겠다는 의미입니다. 이 포인트는 통화 UX를 눈에 띄게 개선합니다.

셋째, 품질 타임라인이 남으면 고객 응대와 내부 디버깅 수준이 달라집니다. 예를 들어 한 세션에서 RTT가 급증했고, 이후 jitter와 packet loss가 이어졌으며, 그 뒤 재연결 횟수가 늘다가 결국 `DISCONNECTED`로 끝났다면 문제의 흐름을 시간순으로 설명할 수 있습니다. 이 정보가 없으면 운영은 항상 추측에 머뭅니다.

넷째, SSE 기반 통화 이벤트 스트리밍은 프론트엔드 상태 일관성을 높입니다. `publishCallEnded`, `publishCallExtended`, heartbeat 주기가 분리되어 있으면, 미디어 품질 문제와 UI 동기화 문제를 따로 볼 수 있습니다. 이는 장애 대응에서 원인 분리에 매우 중요합니다.

  • 연결 실패와 품질 저하를 같은 문제로 뭉개지 않고 분리 분석 가능
  • 사용자 이탈, 재입장, 종료를 서버 기준으로 정합성 있게 처리 가능
  • 세션 단위 품질 리포트로 운영 지표와 개선 우선순위 설정 가능
  • 프론트엔드가 통화 종료/연장 상태를 안정적으로 반영 가능

결국 이 기술 묶음은 “좋은 오디오 스택”이 아니라 “Voice-Link의 통화 신뢰를 설명 가능한 형태로 만드는 운영 체계”라고 보는 편이 정확합니다.

4. 구현 방향

Voice-Link의 구현 방향은 WebRTC 미디어 경로, LiveKit 이벤트, Redis 품질 타임라인, Spring Boot 제어 로직을 하나의 통화 수명주기로 묶는 것입니다.
핵심은 기술을 많이 붙이는 것이 아니라, 연결 성립부터 종료 판정까지 같은 세션 ID를 기준으로 추적 가능하게 만드는 데 있습니다.

우선 접속 경로는 `application.yml`의 `livekit.url`, `livekit.api-url`처럼 외부 시그널링 주소와 서버 측 제어 주소를 분리해 관리하는 편이 안전합니다. 클라이언트는 미디어 연결에 집중하고, 백엔드는 룸 생성·조회·종료 같은 제어를 담당해야 운영 중 경계가 흐려지지 않습니다.

다음으로 TURN/STUN은 “있으면 좋은 옵션”이 아니라 필수 가용성 레이어로 다뤄야 합니다. `docs/turn-stun-flow.md` 기준으로 보면 Voice-Link는 외부 TURN 구성을 전제로 하고 있습니다. 운영에서는 특정 네트워크에서 직접 경로가 막혔을 때도 릴레이 경로가 확보되는지, 그리고 TLS 기반 경로가 마지막 생존선 역할을 하는지를 지속적으로 확인해야 합니다.

통화 종료 처리도 중요합니다. `LiveKitWebhookService`는 `participant_left`나 `participant_disconnected` 이벤트가 들어와도 곧바로 종료하지 않고, 재입장 가능성을 고려해 유예 시간을 둡니다. 이 방식은 실무적으로 매우 중요합니다. 이동 중 네트워크 전환이나 일시적인 오디오 장치 변경을 “실제 통화 종료”로 오인하면 사용자 체감이 크게 나빠집니다.

품질 측정은 단순 평균값 저장으로 끝내면 안 됩니다. `CallQualityService`처럼 세션별 snapshot을 Redis 리스트로 남기고, 최대 개수를 제한하고, TTL을 둬야 운영 비용을 통제하면서도 최근 장애를 복기할 수 있습니다. 특히 RTT, jitter, packet loss, reconnect count, connection state를 한 시계열로 보는 구조가 중요합니다.

STT, VAD, 오디오 전처리도 Voice-Link에서 장기적으로는 같은 세션 흐름 위에 올라가야 합니다. 예를 들어 통화 중 VAD로 무음 구간을 식별하고, 필요한 구간만 STT에 보내 자막이나 녹취 이벤트를 만들려면, 미디어 세션 ID와 후처리 이벤트 ID가 분리되지 않아야 합니다. 다만 이런 기능은 먼저 통화 연결 안정성과 품질 관측이 확보된 뒤 올리는 것이 순서상 맞습니다.

실시간 자막이나 녹취를 붙일 때도 운영 포인트는 동일합니다. 음성 처리 파이프라인은 별도여도, “누가 언제 말했고”, “그 시점 네트워크 품질이 어땠는지”, “재연결 직후 인식률이 떨어졌는지”를 함께 보아야 실제 개선이 가능합니다. 따라서 음성 이벤트 처리 역시 통화 품질 타임라인과 완전히 분리해서 운영하면 안 됩니다.

프론트와의 상태 동기화는 `CallEventStreamingService`처럼 SSE heartbeat를 유지하는 방식이 현실적입니다. WebRTC 자체는 미디어를 책임지지만, 통화 종료 안내, 연장 알림, 세션 만료 안내는 애플리케이션 이벤트 경로가 책임져야 합니다. 운영 중에는 heartbeat 누락률, emitter 정리 실패, 종료 이벤트 중복 발송 여부를 함께 관찰해야 합니다.


public void handleEvent(WebhookEvent event) {
    switch (event.getEvent()) {
        case "room_finished" -> endSession(event);
        case "participant_left", "participant_disconnected" -> scheduleGracefulTermination(event);
        case "participant_joined" -> cancelPendingTermination(event);
        default -> { }
    }
    callQualityService.saveSnapshot(userId, snapshot);
    callEventStreamingService.publishCallExtended(session, extendedSeconds);
}

이 짧은 예시는 Voice-Link 구조의 핵심을 잘 보여줍니다. 미디어 서버 이벤트는 종료 판정으로 이어지고, 품질 snapshot은 별도 타임라인에 남으며, 사용자 화면에는 SSE로 통화 상태가 전파됩니다. 결국 중요한 것은 WebRTC 자체보다 “이후의 운영 로직”입니다.

정리하면 구현 체크포인트는 다음과 같습니다.

  • `application.yml`에서 LiveKit, 매칭, Redis, 품질 보관 정책을 분리 관리할 것
  • 웹훅 이벤트별 종료 의미를 다르게 해석할 것
  • 품질 snapshot은 세션별 시계열로 저장하고 TTL을 둘 것
  • SSE heartbeat로 제어 이벤트 채널의 생존 여부를 계속 확인할 것
  • 향후 STT/VAD/녹취 기능도 동일 세션 흐름 위에 연결할 것

5. 비즈니스 가치

이 기술 스택의 비즈니스 가치는 “통화가 안 될 수도 있는 서비스”를 “문제가 생겨도 복구하고 설명할 수 있는 서비스”로 바꾸는 데 있습니다.
Voice-Link의 핵심 경험은 결국 사람과 사람이 실제로 대화를 시작하고 끝까지 이어가는 것입니다. 연결 실패와 품질 저하는 곧 이탈로 이어지기 때문에, 미디어 기술은 비용이 아니라 매출 보호 장치에 가깝습니다.

TURN/TLS와 NAT traversal이 중요한 이유는 가입자 수보다 먼저 “첫 통화 성공 경험”을 지키기 위해서입니다. 사용자가 한 번 연결 실패를 겪으면 제품에 대한 신뢰를 빠르게 잃습니다. 반대로 복잡한 네트워크에서도 연결이 성사되면 서비스 품질에 대한 인식이 크게 올라갑니다.

LiveKit 웹훅과 유예 종료 로직은 불필요한 통화 종료를 줄여 실제 대화 시간을 늘립니다. 이는 단순 기술 개선이 아니라, 매칭 후 대화 시작률과 대화 지속 시간을 높이는 제품 개선으로 이어집니다.

Redis 기반 call quality timeline은 운영팀과 개발팀의 대응 비용을 낮춥니다. “통화가 안 됐다”는 신고를 받을 때 세션 단위 지표가 남아 있으면, 네트워크 이슈인지, 일시적 재연결인지, 제어 이벤트 누락인지 빠르게 분류할 수 있습니다. 이것은 장애 대응 시간을 줄이고, 잘못된 원인 추정을 줄이는 직접적인 가치가 있습니다.

향후 녹취, 자막, 음성 이벤트 처리까지 확장할 때도 이 기반은 그대로 재사용됩니다. 즉 지금의 WebRTC, TURN, 품질 측정, 이벤트 스트리밍 체계는 단기 연결 안정화뿐 아니라, 장기적으로 음성 AI 기능을 올리기 위한 바닥 공사 역할을 합니다.

요약하면, Voice-Link에서 WebRTC는 기능이 아니라 통화 신뢰를 만드는 운영 기술입니다.
TURN/TLS, LiveKit 웹훅, 품질 타임라인, SSE를 Spring Boot와 일관되게 묶어야 연결 성공률과 장애 대응력이 함께 올라갑니다. 이후 STT, VAD, 녹취 같은 고급 기능도 이 기반 위에서만 안정적으로 확장할 수 있습니다.

댓글 남기기