실시간 스트리밍에서 락 없이 Race Condition을 다루는 법

MyoneeMyonee
4 min read

race condition을 피하고 싶었어

웹서비스를 하다 보면 race condition을 피하기 어려운 경우가 많다. 이를 해결하기 위해 공용 자원에 대한 접근을 제한하거나 락(lock)을 걸기도 하지만, 이 방식은 이 글을 읽는 여러분도 알다시피 서버 성능에 큰 영향을 준다. 그래서 많은 서비스들이 캐시나 메시지 큐 등 다양한 설계를 도입해 이를 보완한다.

스트리밍 서비스 같은 실시간 시스템에서도 race condition은 당연히 발생한다. 다만 일반적인 웹서비스와의 차이점이 있다면, 꼭 대규모 시스템이 아니더라도 이런 이슈가 빈번하게 발생한다는 점이다. 영상 하나를 재생하기 위해서도 수많은 네트워크 요청과 연결 절차가 수반되기 때문인데, 따라서 스트리밍 서비스를 안정적으로 구현하기 위해 다양한 기술적 고려가 필요하다.


스트리밍 서비스는 속도가 생명

여러 캐시 계층이나 이벤트 관리 시스템을 도입해 정합성을 보장하는 것도 좋은 방법이다. 하지만 스트리밍 환경에서는 쉽게 도입하기 어렵다. 일반적인 웹 시스템에서는 정합성을 위해 어느 정도의 latency를 감내할 수 있지만, 스트리밍 서비스는 속도가 깡패기 때문이다.

속도가 얼마나 중요하냐?
반드시 필요한 데이터를 제외, 일부 데이터 유실은 감내할 수 있다는 전제하에 시스템을 설계할 정도로!


스트리밍이라면 역시 이벤트 기반이지

현대 스트리밍 서비스 = 이벤트 기반

Event-driven architecture
실시간 처리, 분산성, 사용자 반응성까지 모두 만족시킬 수 있는 가장 효율적이고 합리적인 구조이기 때문에, 현대 스트리밍 서비스는 대부분 이벤트 기반 아키텍처로 설계된다.

특히, 스트리밍 서비스에서 많이 사용되는 실시간 통신(Real-Time Communication, RTC) 기술 중 하나인 WebRTC는 구조적으로 마이크로서비스 아키텍처(MSA) 와 이벤트 기반 흐름을 따른다.

이러한 구조는 각 서비스 간 느슨한 결합과 비동기 처리를 가능하게 해주며, 초지연성이 요구되는 환경에서 매우 적합하다.

💡 WebRTC란?
웹 브라우저나 앱 간에 영상·음성·데이터를 직접 주고받을 수 있게 해주는 실시간 통신 기술
이 과정에서 필요한 connect, invite 등의 흐름은 시그널링 서버라는 중간 서버를 통해 이벤트 기반으로 처리

💡 시그널링 서버란?
WebRTC에서 사용자들 간의 직접 통신(P2P)을 시작하려면, 먼저 연결 요청·응답, 접속 가능 여부, 연결 대상 정보 같은 초기 신호(Signaling)가 오가야 하는데, 이 과정을 중계해주는 서버

실제 영상/음성 데이터는 이 서버를 통하지 않지만, 연결 상태를 관리하기 위한 이벤트들을 주고받기 위한 중심 역할!

이벤트 기반이라면 Kafka 아닌가요?

Kafka vs WebRTC 시그널링 구조 – 아키텍처 관점 비교

  • Kafka
    → 고신뢰·후행 처리에 적합 (내구성, 재처리, 비동기 흐름에 강점)
    → 구조가 무겁고 지연이 존재해 실시간 signaling엔 부적합

  • WebRTC 시그널링 구조
    지연 최소화, 빠른 반응이 핵심, 실시간 응답을 위한 경량 구조
    → 이벤트 유실 보정은 클라이언트에서 직접 수행

관점KafkaWebRTC
지연 허용성수 ms ~ 수 초 가능밀리초 수준 요구 (지연 거의 불가)
중간 저장소있음 (브로커 큐)없음 (즉시 전달)
장애 발생 시메시지 재처리 가능유실 시 끝 (클라이언트 재처리 필요)
이벤트 신뢰성높음 (commit log 기반)낮음 (유실 가능성 존재, 보완 필요)
보정 방식컨슈머 재처리 / 재시도 큐클라이언트가 병합 및 상태 보정 수행

그래서 안 돼 돌아가

WebRTC 기반의 스트리밍 환경에선 조금의 latency도 감당할 수 없기 때문에,
실시간성을 해칠 수 있는 모든 구조를 제거하고 Overengineering을 피하는 것이 핵심!


이벤트 기반 아키텍처에서 락을 최소화하는 방법

이벤트 처리 담당: 정확한 이벤트 전달
클라이언트: 찰나의 오차나 누락은 수신된 이벤트 기반으로 보정

이런 방식이 가능한 이유는?

  • 스트리밍 서비스는 eventual consistency(일시적 불일치) 를 허용

  • 이벤트 서버는 "전달"까지가 책임 범위

  • 이후 데이터 병합/반영은 클라이언트 책임

이벤트 전달 보장 + 병합 가능한 구조 = 락 없이도 실시간 정합성 유지 가능!

실제 race Condition 사례

아래는 문제 상황을 간소화한 시퀀스다.
(*연동 서버와 사용자 앱은 엄밀히 다른 영역이지만 모두 시그널링 서버의 클라이언트이므로 엔드포인트로 묶어서 표현)

  • A, B, C 는 시그널링 서버 입장에서 각 요청의 식별자

  • 각 색상은 같은 트랜잭션

위 상황은 자주 발생하진 않지만, 충분히 일어날 수 있는 문제다.
그러나 이런 낮은 확률의 race condition까지 모두 정합성을 맞춰 설계하기엔 시스템 성능과 복잡도에 큰 비용이 들게 된다.

느슨한 정합성(약결합)을 허용하면서, 가용성(Availability)을 확보해야 한다!
이벤트 버퍼링 & 병합

초기화 전에 onInvited 이벤트가 수신될 수 있으므로 클라이언트는 해당 이벤트를 버퍼에 저장해 두고,
getSessionList 수신 시 병합해서 처리한다.

방식설명특징
Lock (락)동기화 시점 동안 다른 작업 대기정합성 우선, 성능 저하 가능
이벤트 병합이벤트를 보존하고 상태 초기화 이후 병합실시간성 유지, 유연함

해결 자체는 어렵지 않다. 심지어 지금처럼 이미 이벤트 기반으로 설계되어있는 상황에선 임시 변수 하나 더 만들어 이벤트를 저장하고 적절한 시점에 병합하면 된다.
그런데 좀 찜찜하고 불편한 기분이 든다.(나만 그런 건 아닐거야…)

이거… 클라이언트(엔드포인트)에서 처리하는 게 맞아?
서버(시그널링 서버)가 알아서 처리해줘야 하는 거 아니야?

각 구성 요소의 역할은 어디까지일까?

구성책임
소켓/네트워크 계층(ex. WebSocket, 시그널링 서버)이벤트를 정확히 수신하고, 유실 없이 전달하는 것
비즈니스 로직 계층(클라이언트 앱 or 연동 서버)받은 이벤트를 병합하고 보정하여 원하는 데이터를 추출하는 것

즉, 시그널링 서버 입장에선 이미 이벤트 발행의 책임은 다 했다.
이벤트 발행 시점이 겹쳐 우연히 동시에 발행된 것일 뿐, 이벤트 발행 자체가 누락된 게 아니기 때문이다.

그 이후의 판단과 복구는 비즈니스 로직의 몫이다.

처음엔 솔직히 불편했지만…

결국 이 구조는 “확장성과 성능을 위한 전략적 선택”임을 받아들일 수 있었다.

0
Subscribe to my newsletter

Read articles from Myonee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Myonee
Myonee