비디오 Consumer 수 상수화로 서버 부하 최적화
- 설계
- 사용자 경험
- 최적화

들어가며
Plum은 비대면 강의의 단방향 소통 문제를 해결하기 위해 만든 실시간 화상 강의 서비스다. WebRTC 기반의 mediasoup를 사용해 발표자와 참여자가 실시간으로 영상과 음성을 주고받을 수 있도록 구현했다. 한 강의실에 평균 100명이 참여한다고 가정하고 설계했기 때문에, 참여자가 늘어날수록 서버와 클라이언트 양쪽에서 커지는 리소스 부담을 기능 구현과 동시에 고려해야 하는 상황이었다.
처음에는 모든 참여자의 비디오를 동시에 수신하는 방식으로 구현했다. 기능 자체는 문제없이 동작했지만, Prometheus 모니터링에서 예상치 못한 구조적 문제가 드러났다. 참여자 수가 늘어날수록 mediasoup 서버에서 생성되는 Consumer 수가 N×(N-1)로 폭증하고 있었다. 100명이 참여하면 Consumer 수는 9,900개에 달한다. 실제로 20명 이상이 참여하는 시점부터 서버 CPU가 급증했고, 브라우저에서도 다수의 비디오를 동시에 디코딩하면서 렌더링 프레임이 떨어지기 시작했다. 원인은 모든 참여자의 비디오를 동시에 수신하는 구조 자체에 있었다.

요청 시점 수신은 리소스를 줄였지만 페이지 전환마다 로딩이 걸렸다
페이지에 보이는 참여자의 비디오만 수신하는 방식을 적용했다. 한 페이지에 5명이 표시된다면 해당 5명의 Consumer만 생성하고, 페이지를 벗어난 참여자는 즉시 Consumer를 해제하는 방식이다. 화면에 없는 참여자의 Consumer를 만들지 않으니 리소스 사용량은 크게 줄었다.
문제는 페이지를 전환하는 순간이었다. 새 페이지의 참여자들은 아직 Consumer가 없는 상태라, 전환과 동시에 Consumer를 새로 생성해야 했다. Consumer 생성에는 서버와의 시그널링 과정이 필요해 짧은 지연이 불가피했다. 로딩 스피너로 이 지연을 가릴 수는 있었지만 근본적인 해결은 아니었다. 페이지를 넘길 때마다 매번 로딩 스피너를 마주하는 경험은 화상 강의라는 서비스 특성과 맞지 않았다.

±1 프리패치로 로딩 지연 없이 Consumer를 210개로 고정했다
페이지 전환 지연의 원인은 전환 시점에야 Consumer를 생성한다는 데 있었다. 이를 해결하기 위해 현재 페이지뿐 아니라 앞뒤 페이지의 Consumer를 미리 구독해두는 방식을 적용했다. 페이지당 5명 기준으로 현재 페이지 5명, 앞 페이지 5명, 뒤 페이지 5명, 최대 15명의 Consumer만 유지한다. 페이지를 전환하면 이미 구독된 상태이므로 지연 없이 바로 재생됐다. 동시에 새로운 앞뒤 페이지를 미리 구독하고 멀어진 페이지의 Consumer는 해제해, 항상 최대 15명을 유지하도록 했다.

이 방식으로는 참여자가 100명이라도 동시에 유지되는 Consumer가 최대 15 × 14 = 210개로 고정된다. 전체 수신 방식의 9,900개와 비교하면 97% 이상 절감되는 수치다.

±1이 적절한 범위인지는 별도로 검증했다. 추가 Consumer 수신이 서버와 클라이언트 성능에 미치는 영향을 측정했고, 최대 10개의 추가 Consumer를 수신하는 상황에서도 서버와 클라이언트 모두 성능 저하가 없음을 확인했다.
그렇다면 범위를 ±2로 넓히면 더 나을까 싶어 같은 방식으로 확인해봤다. ±2로 확장하면 구독 인원은 25명, Consumer 수는 25 × 24 = 600개로 ±1의 210개보다 약 3배 늘어난다. 성능 테스트 결과 ±2에서도 저하는 없었지만, ±1에서 이미 앞뒤 페이지가 미리 구독된 상태였기 때문에 사용자가 체감할 수 있는 차이는 없었다. 그래서 ±1을 불필요한 리소스 소모 없이 UX를 보장할 수 있는 최소한의 범위로 판단했다.
정리하며
±1 프리패치 전략을 적용한 결과, 100명 참여 기준으로 Consumer 수가 9,900개에서 210개로 줄었고 참여자 수가 늘어나도 항상 210개로 고정되어 서버 부하를 예측 가능한 범위로 제한할 수 있게 됐다. 실제 유저 57명이 동시 접속한 테스트에서도 CPU 급증 없이 안정적인 연결을 유지했고, 페이지 전환 시에도 로딩 지연 없이 바로 재생됐다.
선택지를 비교할 때는 장점만 볼 게 아니라 단점의 허용 범위를 먼저 정의해야 한다는 걸 배웠다. 요청 시점 수신은 리소스 효율은 좋았지만 페이지 전환마다 로딩 스피너를 띄우는 UX 단점이 허용 범위를 넘었다. ±2는 성능 저하는 없었지만, ±1에서 이미 앞뒤 페이지가 구독된 상태라 사용자가 체감할 차이가 없어 추가 리소스를 쓸 이유가 없었다. 각 선택지의 단점이 허용 가능한 수준인지를 먼저 판단한 뒤 결정하는 과정이 이번 최적화에서 가장 중요했다.