1. 문제
Voice-Link에서 진짜 어려운 지점은 “음성 통화 기능을 붙이는 것”이 아니라, 다양한 네트워크 환경에서 통화를 끝까지 성립시키고 장애 원인을 남기는 것입니다.
실시간 음성 서비스는 브라우저나 앱에서 마이크를 켜는 순간 끝나는 기능이 아닙니다. 같은 코드라도 어떤 사용자는 바로 연결되고, 어떤 사용자는 방 입장은 되지만 음성이 안 들리며, 어떤 사용자는 중간에 끊깁니다.
이 문제는 Voice-Link가 현재 사용 중인 구조만 봐도 드러납니다. `src/main/resources/application.yml`에는 `livekit.url`, `livekit.api-url`, `spring.data.redis.*`, `app.match.*`, `app.call-quality.*` 같은 설정이 분리돼 있는데, 이것은 통화가 단일 서버 기능이 아니라 시그널링, 미디어, 상태 저장, 이벤트 전파가 나뉜 구조라는 뜻입니다.
특히 `docs/turn-stun-flow.md`에서 정리된 흐름처럼, 실제 통화 연결은 WebRTC 후보 수집과 ICE 체크, STUN 기반 외부 주소 확인, TURN 릴레이 확보 여부에 따라 갈립니다. 즉 Voice-Link에서 실패율을 낮추려면 프론트 코드보다 먼저 “어떤 네트워크에서 왜 통화가 안 붙는가”를 다룰 수 있어야 합니다.
또 하나의 문제는 연결 성공 이후입니다. 음성은 붙었지만 지연이 커지거나 재연결이 반복되면 사용자 입장에서는 이미 서비스 품질이 무너진 상태입니다. 그래서 Voice-Link처럼 음성 서비스에 가까운 제품은 단순 성공/실패 로그가 아니라 RTT, jitter, packet loss, reconnect 횟수 같은 통화 품질 타임라인이 필요합니다.
- 통화방 입장 성공 ≠ 음성 전송 성공
- 시그널링 성공 ≠ NAT 환경에서 실제 미디어 연결 성공
- 통화 연결 성공 ≠ 안정적인 품질 유지
- 끊김 발생 후 원인 미기록 = 운영 불가능
2. 왜 필요한가
Voice-Link에서 WebRTC, TURN, 품질 측정, 백엔드 이벤트 처리가 함께 필요한 이유는 “연결 성립”과 “품질 설명 가능성”이 제품 신뢰도를 좌우하기 때문입니다.
실시간 음성 서비스는 사용자가 버튼을 눌렀을 때 즉시 반응해야 합니다. 그런데 이동통신망, 사내망, 공용 와이파이, 이중 NAT 환경에서는 직접 연결만 기대하면 실패율이 높아집니다.
이때 STUN은 사용자가 어떤 공인 주소 후보를 가질 수 있는지 확인하는 역할을 하고, TURN은 직접 경로가 막힐 때 미디어를 우회 중계하는 안전장치가 됩니다. Voice-Link의 관련 문서와 설정 구조를 보면 외부 TURN을 별도 운영하는 방향이며, 내장 TURN에만 의존하지 않고 릴레이 경로를 준비하는 이유도 여기에 있습니다.
Voice-Link에서 이 기술을 어떻게 적용할 수 있는지 가장 분명하게 보여주는 지점이 `LiveKitWebhookService`입니다. 이 서비스는 `room_finished`, `participant_left`, `participant_disconnected`, `participant_joined` 이벤트를 처리하고, 참가자 이탈 직후 곧바로 종료하지 않고 짧은 유예 시간을 둡니다. 이 설계는 모바일 네트워크 전환이나 일시적 재접속 상황을 “즉시 통화 종료”로 오판하지 않기 위해 필요합니다.
`CallQualityService`도 같은 맥락입니다. 이 서비스는 Redis 리스트에 통화 품질 스냅샷을 누적하고, 길이를 제한하고, 보관 만료를 두며, `DISCONNECTED` 상태나 재연결 발생 시 진단 리포트를 만들 수 있게 설계돼 있습니다. 즉 Voice-Link는 이미 “통화가 끊겼다”는 결과보다 “끊기기 전에 어떤 품질 저하가 있었는가”를 남길 수 있는 구조를 갖추고 있습니다.
여기에 `CallEventStreamingService`의 SSE 구조가 더해집니다. 이 서비스는 20초 heartbeat를 보내며 통화 종료나 연장 이벤트를 참가자에게 전파합니다. 이것은 미디어는 WebRTC로 흘리더라도, 제품 상태와 사용자 화면 동기화는 별도 채널로 안정적으로 관리해야 한다는 뜻입니다.
- TURN이 없으면 네트워크 제약 환경에서 연결 실패율이 올라갑니다.
- 웹훅 처리가 없으면 방 종료와 사용자 화면 상태가 어긋납니다.
- Redis 품질 타임라인이 없으면 장애 재현 없이 감으로 대응하게 됩니다.
- SSE 상태 전파가 약하면 “통화는 끝났는데 화면은 진행 중” 같은 UX 오류가 발생합니다.
3. 기대효과
이 구조를 제대로 묶으면 Voice-Link는 통화 연결률, 장애 대응 속도, 품질 개선 우선순위를 모두 더 명확하게 가져갈 수 있습니다.
가장 큰 효과는 연결 실패 원인을 사용자 탓이나 환경 탓으로 뭉개지 않고, TURN 미도달인지, 재접속 빈발인지, 세션 종료 판정 문제인지 분리해서 볼 수 있다는 점입니다.
예를 들어 WebRTC 세션에서 후보 타입과 전송 방식, RTT, jitter, packet loss를 함께 저장하면 “음성이 끊겼다”는 문의를 하나의 현상으로 보지 않게 됩니다. relay 경로 비중이 급증했는지, 특정 구간에서 round trip time이 튀는지, 재연결 직전 packet loss가 높았는지 같은 운영 신호가 생깁니다.
Voice-Link의 현재 구조에서는 이 데이터를 단발성 로그보다 타임라인으로 보는 것이 중요합니다. `app.call-quality.retention-seconds`와 `app.call-quality.max-snapshots-per-session` 같은 설정 키가 있는 이유도, 품질 수집이 영구 보관용 데이터가 아니라 장애 진단과 운영 관측을 위한 압축형 세션 기록이기 때문입니다.
또한 `participant_left`나 `participant_disconnected`를 곧바로 종료 처리하지 않고, 재입장 여부를 확인한 뒤 종료하는 로직은 사용자 체감 품질을 높입니다. 짧은 네트워크 흔들림을 즉시 종료로 처리하면 제품은 “끊김이 잦은 서비스”가 되지만, 유예 시간을 두면 “잠깐 불안정해도 복구되는 서비스”가 됩니다.
운영 측면에서도 효과가 큽니다. 미디어 서버, TURN 서버, 백엔드, Redis, 프론트 상태 전파가 각각 따로 놀면 장애가 생겼을 때 팀이 어디부터 봐야 할지 모릅니다. 반대로 통화 품질 타임라인과 웹훅, SSE 흐름이 맞물려 있으면 “세션 종료 시각”, “마지막 품질 상태”, “재연결 여부”, “사용자 화면 반영 여부”를 같은 사건으로 묶어 볼 수 있습니다.
- 연결 실패율 감소: TURN/TLS 기반 우회 경로 확보
- 오탐 종료 감소: 참가자 이탈 후 짧은 재입장 유예
- 장애 분석 속도 향상: Redis 품질 타임라인 기반 진단
- UX 안정성 향상: SSE heartbeat로 상태 동기화 유지
4. 구현 방향
Voice-Link에서 우선 구현해야 할 방향은 “통화 성립 경로”, “품질 스냅샷”, “종료 판정”, “클라이언트 상태 반영”을 한 흐름으로 묶는 것입니다.
핵심은 기술을 많이 붙이는 것이 아니라, 현재 존재하는 LiveKit, TURN, Redis, SSE, Spring Boot 구조를 운영 가능한 형태로 정렬하는 데 있습니다.
첫째, `application.yml`의 `livekit.url`, `livekit.api-url`, `spring.data.redis.*`, `app.match.*`, `app.call-quality.*`는 단순 설정이 아니라 운영 기준점입니다. 통화방 생성과 토큰 발급, 품질 스냅샷 저장, 세션 만료 판단, 관리자 진단 API의 기준이 모두 이 레이어를 통과해야 합니다.
둘째, TURN 운영은 “있다”보다 “어떤 환경에서도 붙는다”가 중요합니다. 관련 문서와 coturn 설정 구조를 보면 UDP뿐 아니라 TCP와 TLS 경유 경로를 준비하는 방향인데, 이는 제한된 네트워크에서 연결 생존율을 높이는 실무 선택입니다. 실제 운영에서는 계정 정보나 인증서 경로를 공개하면 안 되며, 글과 문서에서는 키 이름과 역할 수준까지만 다루는 것이 맞습니다.
셋째, 품질 수집은 평균값보다 이벤트 순간을 잡아야 합니다. `CallQualitySnapshotRequest` 구조에는 `jitterMs`, `packetLossRatio`, `roundTripTimeMs`, `publisherLocalCandidateType`, `subscriberTransport`, `reconnectCount`, `lastReconnectDurationMs` 같은 필드가 들어갑니다. 이 조합은 “느렸다”가 아니라 “어떤 ICE 경로에서, 어느 시점에, 어떤 재연결 패턴이 있었는가”를 설명하는 데 유용합니다.
넷째, 웹훅 종료 처리와 SSE 전파는 분리하되 연결돼 있어야 합니다. LiveKit 웹훅이 세션 종료를 감지하면 백엔드가 최종 종료를 판정하고, `CallEventStreamingService`가 이를 사용자에게 밀어줘야 합니다. 그래야 서버는 종료됐는데 클라이언트 타이머가 계속 도는 현상을 줄일 수 있습니다.
- 연결 단계: LiveKit 토큰 발급, ICE 서버 전달, TURN 우회 경로 확보
- 수집 단계: 품질 스냅샷을 Redis 리스트에 저장, 길이 제한, 만료 설정
- 판정 단계: disconnect와 reconnect를 구분해 진단 리포트 생성
- 전파 단계: SSE heartbeat와 종료 이벤트로 화면 상태 일치
public void saveSnapshot(CallQualitySnapshotRequest request) {
String key = "call:quality:" + request.sessionId() + ":timeline";
redisTemplate.opsForList().rightPush(key, writePayload(request));
redisTemplate.opsForList().trim(key, -maxSnapshotsPerSession, -1);
redisTemplate.expire(key, Duration.ofSeconds(retentionSeconds));
if ("DISCONNECTED".equalsIgnoreCase(request.connectionState())
|| (request.reconnectCount() != null && request.reconnectCount() > 0)) {
diagnosisReporter.report(readTimeline(request.sessionId()));
}
}
위와 같은 로직이 중요한 이유는 간단합니다. Voice-Link에서는 통화 품질을 통계 대시보드용 숫자로만 남기는 것이 아니라, 세션 단위 사건 기록으로 보존해야 이후 웹훅 종료 이벤트와 연결해서 해석할 수 있기 때문입니다. 이 흐름이 있어야 “연결 실패”, “중간 끊김”, “재접속 후 복구”를 서로 다른 운영 사건으로 다룰 수 있습니다.
5. 비즈니스 가치
Voice-Link에서 이 구조의 비즈니스 가치는 단순한 기술 완성도가 아니라, 통화 신뢰도를 제품 경쟁력으로 바꾸는 데 있습니다.
실시간 음성 서비스는 사용자가 한 번만 “안 붙는다”, “끊긴다”, “상대가 안 들린다”를 겪어도 재사용 의사가 크게 떨어집니다. 따라서 WebRTC 연결 생존성과 품질 추적 가능성은 곧 리텐션 문제입니다.
또한 이 구조는 CS 대응 비용을 줄입니다. 운영자가 세션별 품질 타임라인과 종료 이벤트를 확인할 수 있으면, 막연한 감정 대응이 아니라 실제 연결 상태를 기준으로 설명할 수 있습니다. 이것은 단순히 로그를 많이 남기는 것보다 훨씬 큰 차이를 만듭니다.
향후 녹취, 자막, 음성 이벤트 처리, STT, VAD 같은 기능을 붙이더라도 먼저 안정적인 실시간 음성 경로가 필요합니다. 오디오가 불안정한 상태에서 후단 인식 기능을 고도화해도 결과 품질은 흔들립니다. 즉 Voice-Link에서 음성 AI 기능의 품질은 결국 WebRTC 연결 품질과 TURN 운영, 세션 상태 판정의 품질 위에 세워집니다.
정리하면, Voice-Link에 필요한 것은 범용적인 “실시간 서비스 트렌드”가 아닙니다. 지금 코드베이스에 이미 존재하는 LiveKit 웹훅, TURN 경로, Redis 품질 타임라인, SSE heartbeat, Spring Boot 설정을 하나의 운영 모델로 묶는 일입니다. 그렇게 해야 통화가 붙는 서비스에서 끝나지 않고, 통화 품질을 설명하고 개선할 수 있는 서비스가 됩니다.
요약하면 Voice-Link의 실시간 음성 품질은 WebRTC 자체보다 TURN 생존성, 품질 타임라인 수집, 종료 이벤트 처리, 상태 전파를 함께 설계하느냐에 달려 있습니다.
지금 구조에서 가장 중요한 개선 포인트는 연결 실패를 줄이는 TURN 운영과, 끊김 원인을 Redis 기반 타임라인으로 남겨 운영 가능한 서비스로 만드는 것입니다.