두 사용자가 같은 위치를 동시에 편집했을 때 — 낙관적 로컬 적용, '한 번에 하나만 전송' 규칙, 서버의 변환·revision 확정·브로드캐스트를 거쳐 전원이 'Hello world!'로 수렴하기까지를 21단계로 따라간다.
1 / 21
1 / 21Luiz와 John이 같은 빈 문서를 엽니다. 각 클라이언트는 네 가지를 추적합니다 — 마지막 동기화 revision, 미전송 변경(pending), 전송 후 미승인 변경(sent), 현재 문서 상태입니다.서버가 기억하는 것도 세 가지뿐입니다 — 미처리 변경 목록, revision log, 현재 문서 상태. 이 상태들이 프로토콜의 전부입니다.
OT의 핵심은 diff 병합이 아니라 좌표 보정이다 — 모든 변경이 '어느 revision을 보고 만들어졌는지'를 갖고 다니고, 서버와 클라이언트가 같은 변환 함수로 위치·범위를 상호 보정하므로 반드시 같은 문서로 수렴한다.
왜 헷갈리는가
'충돌하면 한쪽이 이기고 다른 쪽은 버려지겠지'가 흔한 오해다. OT에는 패자가 없다 — 같은 위치 @6에 동시에 입력해도 서버 도착 순서가 확정 순서를 정할 뿐, 늦게 온 변경은 버려지는 게 아니라 위치가 변환되어(@6→@12) 살아남는다. 또 하나: 편집은 서버 승인을 기다리지 않는다. 화면에 먼저 반영(낙관적 적용)하고, 어긋남은 변환으로 나중에 맞춘다.
애니메이션이 보여주는 것
클라이언트가 추적하는 4가지 상태(마지막 동기화 revision · 미전송 pending · 미승인 sent · 현재 문서)와 서버의 3가지 상태(미처리 변경 · revision log · 문서)를 노드로 놓고, Luiz가 'Hello'를 치는 순간부터 따라간다. 변경은 pending에 쌓였다가 'revision N 기준' 꼬리표를 달고 하나씩 전송되고, 서버는 도착 순서대로 변환·확정해 revision을 부여하고 나머지 참여자에게 브로드캐스트한다.
하이라이트는 동시 편집 충돌 국면이다 — Luiz의 ' world'@6과 John의 '!'@6이 같은 자리를 겨눈다. 먼저 도착한 ' world'가 Revision 2가 되고, John의 '!'는 서버에서 'John이 마지막으로 동기화한 이후 확정된 모든 변경'에 대해 변환되어 @12로 보정된 뒤 Revision 3이 된다. 세 곳의 문서가 전부 'Hello world!'로 수렴한다.
왜 한 번에 하나만 전송하는가
sent 목록에는 미승인 변경이 최대 1건이다. ack가 와야 pending의 다음 변경을 보낸다. 미승인 변경이 항상 하나뿐이면 수신한 브로드캐스트와 내 변경 사이의 변환 경로가 하나로 고정되고, 서버는 클라이언트별 상태를 전혀 기억할 필요가 없다 — 변경에 실려 온 revision 번호만 보면 무엇에 대해 변환할지 알 수 있다. 편집자가 늘어도 서버 복잡도가 일정한 이유다.
기다리는 동안 타이핑이 막히는 것도 아니다. 변경은 로컬에 즉시 적용되고 pending에 쌓이며, 연속된 연산은 합성(composition)해 한 건으로 보낼 수 있다. 이 구조의 이론 기반이 Jupiter(UIST '95) — 중앙 서버를 유일한 순서 결정자로 두고 각 클라이언트는 서버와 1:1로만 변환하는 클라이언트-서버 OT다.
텍스트만이 아니다 — 스타일 변환과 오프라인 병합
기본 변경 유형은 InsertText, DeleteText, ApplyStyle 세 가지고 변환 규칙은 모두에 적용된다. {ApplyStyle bold @10-20}과 동시에 범위 한가운데 {InsertText 'ABC' @15}가 확정되면 bold는 {ApplyStyle bold @10-23}으로 늘어난다. 반대로 이탤릭과 색상처럼 충돌하지 않는 변경은 변환 없이 공존한다.
오프라인 편집도 같은 기계장치의 연장이다 — 단절 중엔 로컬 적용 + pending 누적, 재접속하면 못 본 revision들을 먼저 받아 pending을 변환한 뒤 평소처럼 하나씩 전송한다. 별도의 병합 코드가 없다.
같은 위치 동시 입력 → 서버 도착 순서가 확정 순서, 어느 쪽도 유실 없음
삽입이 스타일 범위를 관통하면 범위가 삽입 길이만큼 확장
서버 문서는 revision log를 재생한 결과 — 확정 이력이 진실
기억할 것
클라이언트 4상태(revision·pending·sent·문서), 서버 3상태(미처리·log·문서) — 이게 프로토콜의 전부다.
낙관적 적용: 화면 먼저, 승인 나중 — 어긋남은 변환으로 맞춘다.
sent는 최대 1건: 변환 경로가 하나로 고정되고, 서버는 클라이언트별 상태가 필요 없다.
충돌은 승자 독식이 아니다 — 늦은 변경은 위치가 변환되어 살아남는다(@6→@12).
오프라인 병합도 같은 루프다: 밀린 revision 수신 → pending 변환 → 하나씩 재전송.