여러 사본이 각자 독립적으로 수정돼도, 변경이 어떤 순서로 도착하든 병합하면 항상 같은 상태로 수렴하도록 수학적으로 설계한 자료구조. 이름 그대로 "충돌 없는 복제 데이터 타입"이다. 중앙 서버가 순서를 정해 주지 않아도 된다.
두 가지 방식
- 상태 기반(State-based, CvRDT): 상태 전체를 주고받고 병합 함수로 합친다. 병합은 순서와 중복에 상관없이 같은 결과를 내야 한다
- 연산 기반(Operation-based, CmRDT): 연산을 주고받되, 동시에 일어난 연산끼리는 순서를 바꿔 적용해도 결과가 같도록 설계한다. 대신 전달 계층이 연산을 빠짐없이 한 번씩, 인과 순서대로 전달해야 한다
// G-Counter: 증가만 하는 카운터. 노드마다 자기 칸만 올리고, 병합은 칸별 최댓값
type GCounter = Record<string, number>
const merge = (a: GCounter, b: GCounter) => {
const out = { ...a }
for (const [node, n] of Object.entries(b)) out[node] = Math.max(out[node] ?? 0, n)
return out
}
const value = (c: GCounter) => Object.values(c).reduce((s, n) => s + n, 0)주요 타입
| 타입 | 용도 | 방식 |
|---|---|---|
| G-Counter / PN-Counter | 카운터 | 노드별 칸, 증가·감소 칸 쌍 |
| LWW-Register(Last-Writer-Wins) | 단일 값 | 타임스탬프가 늦은 쪽이 이김 |
| OR-Set(Observed-Remove Set) | 집합 | 추가마다 고유 태그를 붙여 추적 |
| RGA(Replicated Growable Array), YATA 계열 | 텍스트·리스트 | 문자마다 고유 ID |
텍스트 편집에서는 문자에 고유 ID를 붙이고 "어느 문자 뒤에 넣는다"를 인덱스가 아닌 ID로 표현해, 동시에 넣어도 위치가 꼬이지 않는다.
한계
- 삭제된 요소도 표시(툼스톤, Tombstone)로 남겨야 해서 메타데이터가 계속 쌓인다. 정리 전략이 필요하다
- "재고가 음수가 되면 안 된다" 같은 업무 규칙이나 의도 기반 연산은 자동 병합으로 표현하기 어렵다
오프라인에서 각자 고친 뒤 나중에 합치는 로컬 퍼스트와 잘 맞고, 서버가 연산을 변환하는 OT와 자주 비교된다. 각 사본이 언젠가 같아진다는 점에서 최종 일관성의 한 구현이다. 라이브러리로는 Yjs, Automerge가 대표적이다(2026 기준).