실시간 협업의 난제들
실시간 협업은 다양한 기술적 난제를 수반합니다. 여기에는 WebSocket 연결 관리, 최대한 낮은 지연 시간 유지, 연결 끊김 후에도 데이터 손실 없이 히스토리 재생, 동시 편집 및 순서 이탈 배달 처리, 커서 및 타이핑 인식, 그리고 효율적인 데이터 영구 저장이 포함됩니다.
AnyCable: 고성능 WebSocket 서버
Action Cable은 Rails의 WebSocket 기능이지만, 연결 수가 증가할수록 브로드캐스트 지연 시간과 리소스 사용량이 급증하는 경향이 있습니다. AnyCable은 Action Cable 호환 WebSocket 서버로 Go 언어로 작성되어 이러한 문제를 해결합니다.
-
성능 이점: 벤치마크 결과, AnyCable은 Action Cable 대비 연결 수가 많아질수록 브로드캐스트 지연 시간이 현저히 낮고(10,000 연결 시 1초 미만), 메모리 및 CPU 사용량도 훨씬 효율적입니다.
-
아키텍처: AnyCable은 Rails 웹 서버와 WebSocket 서버를 분리하여 확장성을 높입니다. Rails는 웹 요청을, AnyCable은 WebSocket 연결을 전담하며, HTTP 또는 gRPC를 통해 Rails와 통신합니다. 이로써 Ruby 비즈니스 로직을 그대로 유지하면서 고성능 WebSocket을 활용할 수 있습니다.
-
Reliable Streams: AnyCable은 연결이 끊겼다가 다시 연결될 때 메시지 히스토리를 자동으로 재생하는 ‘Reliable Streams’ 기능을 제공하여 데이터 손실 없이 사용자 경험을 개선합니다.
YJS와 CRDT: 충돌 없는 동시 편집
CRDT(Conflict-free Replicated Data Type)는 분산 시스템에서 동시 편집 시 충돌 없이 데이터가 항상 일관된 최종 상태로 수렴하도록 설계된 특수 데이터 구조입니다. YJS는 가장 인기 있는 CRDT 구현체 중 하나입니다.
-
작동 원리: YJS는 모든 변경 사항을 ‘아이템’이라는 형태로 표현하며, 내부적으로 연결 리스트 CRDT를 사용합니다. 각 아이템은 클라이언트 ID와 램포트 타임스탬프(Lamport timestamp)로 구성된 고유 ID를 가집니다. 이는 독립적인 시스템 간의 순서를 보장하며, 절대적인 위치나 시간 대신 아이템 간의 관계를 저장하여 동시 편집을 처리합니다.
-
데이터 유형: YJS는 텍스트, 맵, XML 요소 등 다양한 데이터 유형을 지원하여 개발자가 CRDT의 복잡한 내부 구조를 직접 관리할 필요 없이 쉽게 사용할 수 있도록 합니다.
-
오프라인 지원: 클라이언트가 오프라인 상태에서 편집한 내용도 다시 연결될 때 충돌 없이 병합됩니다.
커서 및 타이핑 인식과 AnyCable Whisper
실시간 협업에서 다른 사용자의 커서 위치나 타이핑 상태를 시각적으로 보여주는 것은 매우 중요합니다. YJS는 ‘awareness’ 클래스를 통해 이를 위한 프로토콜을 제공하며, 이 또한 내부적으로 CRDT를 사용하여 일관성을 유지합니다.
- AnyCable Whisper: 커서 위치와 같은 실시간성 높은 임시 데이터는 영구 저장할 필요가 없습니다. AnyCable의 ‘Whisper’ 기능은 Rails 애플리케이션 서버를 거치지 않고 AnyCable 서버가 직접 클라이언트 간에 메시지를 전달하게 하여, Rails의 부하를 줄이고 초저지연 통신을 가능하게 합니다.
영구 저장 및 Compaction
CRDT는 모든 변경 사항을 개별 업데이트 패키지로 저장하므로, 시간이 지남에 따라 저장해야 할 데이터의 양이 기하급수적으로 증가합니다. 이를 해결하기 위해 ‘Compaction’ 과정이 필요합니다.
-
Compaction: 여러 업데이트를 하나의 단일 업데이트로 압축하여 저장 공간을 최적화하고, 초기 동기화 시 전송해야 할 데이터 양을 줄입니다.
-
YRB: Rust로 구현된 YJS 래퍼인 YRB Gem을 사용하여 Ruby 서버 측에서 YJS 문서에 대한 Compaction 및 기타 작업을 수행할 수 있습니다. 다만, 현재는 스레드 안전성 문제가 있어 뮤텍스(mutex)를 통한 조정이 필요합니다.