본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

실시간 공동 편집: OT와 CRDT

작성 2026. 10. 31.추가
목차5개 항목
출처와 참고

ByteByteGo의 System Design 101 가이드 「How to Design Google Docs」를 바탕으로 재구성한 노트다. 차등 동기화는 Neil Fraser의 「Differential Synchronization」과 그 구현인 「MobWrite」로 보강했다.

무엇이 어려운가

두 사람이 같은 문장을 동시에 고치면, 각자의 화면에서는 자기 편집이 먼저 적용된 상태다. 서로의 편집을 그냥 받아 적용하면 위치가 어긋나 두 화면의 결과가 달라진다.

flowchart TB
  O["원문: #quot;abc#quot;"] -- "Alice: 0번 위치에 #quot;X#quot; 삽입" --> A["#quot;Xabc#quot;"]
  O -- "Bob: 2번 위치의 #quot;c#quot; 삭제" --> B["#quot;ab#quot;"]
  B -- "Bob의 화면에 Alice의 연산을 그대로 적용" --> BR["#quot;Xab#quot;<br>(맞음)"]
  A -- "Alice의 화면에 Bob의 #quot;2번 삭제#quot;를 그대로 적용" --> AW["#quot;Xbc#quot;<br>(틀림: c가 아니라 b가 지워짐)"]
  A -- "Bob의 연산을 #quot;3번 삭제#quot;로 바꿔 적용" --> AR["#quot;Xab#quot;로 수렴한다"]

공동 편집의 목표는 모든 참여자의 문서가 결국 같은 상태로 수렴하고, 각자의 의도가 보존되는 것이다.

시스템 구조

flowchart LR
  cl["클라이언트들"] -->|"편집 연산"| ws["WebSocket 서버"] --> mq["메시지 큐"] --> fo["파일 연산 서버<br/>충돌 해결 알고리즘으로 연산 변환·적용"]
  fo -->|"변환된 연산 브로드캐스트"| cl
  subgraph store["저장소"]
    meta["파일 메타데이터(제목, 권한)"]
    body["파일 내용(스냅숏)"]
    log["연산 기록(로그)"]
  end
  • 클라이언트는 문서 전체가 아니라 연산(삽입·삭제·서식) 을 보낸다. 작고 자주 보낼 수 있다.
  • 실시간 전달은 WebSocket 서버가 맡는다 → WebSocket
  • 연산은 큐에 순서대로 쌓아 유실 없이 파일 연산 서버가 처리한다.
  • 저장은 스냅숏 + 연산 로그다. 로그를 재생하면 어느 시점이든 되살릴 수 있어 버전 기록과 되돌리기가 자연스럽다. 로그가 길어지면 주기적으로 스냅숏을 새로 만든다.

충돌 해결 알고리즘

방식원리장점단점
OT(Operational Transformation, 운영 변환)동시에 일어난 연산을 서로에 맞게 변환해 적용한다. 보통 중앙 서버가 순서를 정한다오래 검증됐다. 연산이 작다변환 함수를 연산 종류마다 정확히 짜기 어렵다. 중앙 서버가 필요하다
CRDT(Conflict-free Replicated Data Type)문자마다 고유 ID를 붙이는 등 자료구조 자체를 어떤 순서로 합쳐도 같은 결과가 나오게 설계한다중앙 서버 없이도 수렴한다. 오프라인 편집·P2P에 맞다메타데이터가 커질 수 있다. 의도 보존이 미묘한 경우가 있다
차등 동기화(Differential Synchronization)상대 사본과의 차이(diff) 를 계산해 주고받고 패치한다구현이 단순하다충돌이 잦으면 패치가 실패하고 정밀도가 떨어진다
  • 구글 문서는 OT 계열로 알려져 있다.
  • CRDT는 Yjs·Automerge 같은 라이브러리로 널리 쓰인다. 로컬 우선(local-first) 앱, 오프라인 후 동기화, 여러 서버 간 복제에 특히 맞는다.
  • 텍스트 외에 커서 위치·선택 영역 같은 현재 상태(presence) 는 저장하지 않고 실시간으로만 브로드캐스트한다.

차등 동기화

OT·CRDT가 연산을 주고받는다면, 차등 동기화(differential synchronization) 는 문서의 차이(diff) 를 주고받는다. 클라이언트와 서버가 각자 상대 쪽 마지막 상태의 사본인 섀도(shadow) 를 들고, 끝없이 이 순환을 돈다.

1. diff(클라이언트 문서, 클라이언트 쪽 섀도) → 편집 목록
2. 클라이언트 섀도 = 클라이언트 문서 (복사)
3. 편집 목록을 서버로 보낸다
4. 서버: 서버 섀도에 정확히 패치, 서버 문서에는 퍼지 패치
5. 이제 같은 절차를 서버 → 클라이언트 방향으로 반복
  • 핵심은 퍼지 패치다. 문서가 그사이 바뀌었어도 주변 문맥으로 위치를 찾아 패치를 적용한다. 일부 패치가 실패해도 다음 diff에 「되돌림」으로 나타나 자동으로 정리된다.
  • 편집 이벤트를 하나도 빠짐없이 잡을 필요가 없다(잘라내기, 붙여넣기, 자동 완성, 맞춤법 교정까지 diff에 다 드러난다). 메시지 하나를 잃어도 다음 순환에서 다시 맞춰지므로 불안정한 네트워크에 강하다.
  • 소유권 방식(문서나 구획을 잠가 한 명만 편집)은 실시간 협업이 안 되고, 이벤트 전달 방식은 이벤트 하나를 잃으면 이후 편집의 위치가 어긋나 갈라지며, 문서를 통째로 보내 3방향 병합하는 방식은 타이핑 중에 상대 변경을 받지 못하는 반이중이다. 차등 동기화는 이 셋의 약점을 피한다.
  • 대신 diff 계산 비용이 문서 크기에 비례하고, 서버가 클라이언트마다 섀도를 들어야 한다. 폼·스프레드시트처럼 구조화된 데이터에는 더 효율적인 방법이 있다.

정리

  • 공동 편집은 문서가 아니라 연산을 주고받고, 동시에 일어난 연산을 합쳐 모두가 같은 결과로 수렴하게 하는 문제다.
  • 구조는 WebSocket(실시간) + 큐(순서·유실 방지) + 연산 서버(충돌 해결) + 스냅숏·연산 로그 저장이다.
  • OT는 중앙에서 변환하고, CRDT는 자료구조가 순서와 무관하게 수렴한다. 오프라인·분산이 중요하면 CRDT를 본다.

관련 노트: WebSocket, 채팅 시스템 설계: 연결, 접속 상태, 메시지 전달, 현실 시스템을 움직이는 알고리즘과 자료구조, 메시지 큐와 Kafka

원본 기록4

댓글

GitHub 계정으로 댓글을 남길 수 있어요.