Summary#
Bidirectional sync에서 “양방향”은 단순히 양쪽 API를 번갈아 호출한다는 뜻이 아니라, 각 변경의 출처(origin), 권한(authority), 적용 순서(causality), 병합 책임(ownership), 수렴 조건(convergence invariant) 을 계약으로 고정해야 한다는 뜻이다. 이 계약이 없으면 A→B로 보낸 변경이 B→A 피드에 다시 나타나 “echo”로 재적용되거나, 양쪽이 모두 source-of-truth처럼 동작해 값이 흔들리거나, peer-to-peer topology에서 충돌이 조용히 last-write-wins로 덮인다.
재사용 가능한 core-sync 설계에서는 다음 네 가지 축을 분리해야 한다.
- Origin tagging / echo suppression: 변경 이벤트에
origin_node_id,actor_id,operation_id,sync_session_id,causation_id같은 식별자를 부여하고, 수신자는 “내가 만든 변경의 반사”와 “타자가 만든 새 변경”을 구분한다. - Loop detection: 단일 echo만 막는 것이 아니라 A→B→C→A 같은 routing loop를 감지하기 위해 처리한 operation ID, causal chain, checkpoint를 일정 기간 보관한다.
- Ownership topology contract: 객체·필드·관계 단위로 누가 권위자인지 정한다. master-slave, hub-and-spoke, partitioned ownership, multi-master/peer-to-peer는 서로 다른 충돌·병합 계약을 요구한다.
- Convergence invariants: 모든 복제본이 동일한 변경 집합을 관찰한 뒤에는 같은 상태에 도달해야 한다. 이를 위해 idempotent operation, deterministic merge rule, causal metadata, revision/vector clock 또는 CRDT 같은 구조가 필요할 수 있다.
Key Points#
- Echo suppression은 “timestamp 비교”가 아니라 “operation identity 비교”에 가깝다.
updated_at > last_sync_time같은 조건만으로는 내가 방금 보낸 변경이 상대 시스템의 change feed에 다시 나타났을 때 echo인지 새 변경인지 구분하기 어렵다.- 안정적인 패턴은 각 변경에 전역적으로 고유한
operation_id또는 source별 unique change ID를 부여하고, outbound log와 inbound log에서 이미 본 operation을 재적용하지 않는 것이다. -
단, operation ID는 무한 보관하기 어렵기 때문에 retention window, checkpoint, replay 정책이 함께 필요하다.
-
Origin tag와 actor tag는 분리해야 한다.
actor_id는 사용자를 뜻하고,origin_node_id는 변경을 처음 생성하거나 외부로 내보낸 시스템/디바이스/connector를 뜻한다.- 같은 사용자가 모바일과 웹에서 동시에 편집할 수 있으므로 actor만으로 echo를 판단하면 정상 변경을 잘못 무시할 수 있다.
-
권장 최소 메타데이터:
origin_node_idorigin_systemoperation_identity_idbase_revision또는observed_versioncausation_id/correlation_idsync_session_id또는 connector run ID- server-issued checkpoint / cursor
-
sync_session_id는 디버깅과 일시적 echo 억제에는 유용하지만, 영구적인 정합성 키로 쓰면 약하다. - 세션은 재시작, retry, partial failure, backfill, delayed webhook에서 끊길 수 있다.
- 따라서 장기 loop detection은 session ID보다 operation ID, entity revision, change sequence, vector/version metadata에 의존해야 한다.
-
session ID는 “이 실행에서 내가 push한 변경이 곧바로 pull feed에 나타나는가?”를 추적하는 보조 키로 적합하다.
-
Loop detection은 “이미 처리한 operation인가?”와 “같은 상태로 다시 수렴하는가?”를 모두 다뤄야 한다.
- 네트워크 retry나 webhook 재전송은 정상적으로 중복 전달될 수 있으므로 inbound apply는 idempotent해야 한다.
- A→B→A echo만 고려하면 hub-and-spoke 또는 mesh topology에서 B→C→A 경로를 놓칠 수 있다.
-
처리 로그에는 최소한 operation ID, source, target, entity ID, observed revision, apply result, checkpoint를 남겨야 한다.
-
Source-of-truth는 시스템 단위보다 더 세밀하게 정하는 편이 안전하다.
- “CRM이 고객의 source-of-truth” 같은 문장은 실제 sync 계약으로는 부족하다.
- 이름, 이메일, 권한, billing status, 삭제 상태, 태그, 외부 ID mapping 등 필드마다 authority가 다를 수 있다.
- 예시:
- CRM owns:
company_name,sales_owner - Identity provider owns:
email,login_enabled - Billing system owns:
subscription_status - Local app owns:
preferences,last_seen_at
- CRM owns:
-
이처럼 field-level 또는 domain-level ownership을 선언하면 양방향 sync에서도 모든 필드를 multi-master로 취급하지 않아도 된다.
-
Topology별 계약이 다르다.
- Master-slave / primary-replica
- 단일 authoritative side가 최종 상태를 결정한다.
- replica에서 발생한 변경은 command/proposal로 primary에 제출되고, primary의 accepted revision만 canonical state가 된다.
- Hub-and-spoke
- hub가 routing, deduplication, global ID mapping, conflict queue를 담당한다.
- spoke 간 직접 echo를 막으려면 hub가 origin metadata를 보존해야 한다.
- Partitioned ownership
- entity나 field별 owner가 다르다.
- owner가 아닌 쪽의 변경은 거절, queue, 또는 owner-specific command로 변환된다.
-
Peer-to-peer / multi-master
- 모든 peer가 쓰기를 만들 수 있다.
- 단순 revision token만으로는 충분하지 않을 수 있으며, vector clock, CRDT, operational transform, deterministic merge policy 중 하나가 필요할 수 있다.
-
Convergence invariant는 명시적으로 테스트 가능한 형태여야 한다.
- 좋은 invariant 예시:
- 같은 operation은 여러 번 수신되어도 한 번 적용한 결과와 같다.
- 같은 causal history를 가진 복제본은 같은 materialized state를 계산한다.
- owner가 정해진 필드는 owner가 승인한 revision만 canonical value가 된다.
- 충돌이 자동 병합 불가능하면 silent overwrite가 아니라 conflict state로 남는다.
- 삭제와 재생성은 같은 ID 재사용 여부, tombstone retention, alias mapping 규칙에 의해 결정된다.
-
나쁜 invariant 예시:
- “최신 값이 이긴다.”
- “충돌은 거의 없다.”
- “양쪽이 알아서 맞는다.”
- “timestamp가 더 큰 쪽을 사용한다.”
-
Causal ordering은 모든 sync에 필요한 것은 아니지만, 필요한 도메인에서는 대체하기 어렵다.
- 독립 필드 업데이트만 있다면 revision guard와 deterministic merge로 충분할 수 있다.
- 하지만 문서 편집, 카운터, 권한 변경, parent-child 관계, delete-then-update 같은 도메인은 causal order가 깨지면 의미가 달라진다.
- version vector/vector clock은 concurrent update와 happened-before 관계를 표현하는 전통적 방법이다.
-
CRDT는 특정 데이터 타입에서 concurrent operation을 수렴 가능하게 만들지만, 모든 비즈니스 규칙을 자동으로 해결하지는 않는다.
-
Ownership-based merge policy는 CRDT보다 단순하고 감사 가능할 때가 많다.
- 예를 들어
subscription_status는 billing system만 수정 가능하게 하고, 외부 시스템의 값은 무시하거나 warning으로 남기는 것이 multi-master merge보다 안전하다. display_name처럼 낮은 위험의 필드는 last-writer-wins를 허용할 수 있지만, writer의 authority와 timestamp source를 명확히 해야 한다.-
email,role,permission,delete같은 필드는 충돌 시 자동 병합보다 human review 또는 authoritative owner 승인이 더 적절할 수 있다. -
Change feed cursor와 entity revision을 혼동하면 안 된다.
- cursor/checkpoint는 “어디까지 변경 피드를 읽었는가”를 나타낸다.
- entity revision/version은 “이 객체의 어떤 버전을 기준으로 수정했는가”를 나타낸다.
- operation ID는 “이 변경 명령 자체가 무엇인가”를 나타낸다.
-
이 세 값을 하나로 합치면 retry, partial apply, echo suppression, conflict detection이 모두 취약해진다.
-
실무 계약 초안 구조
Node identity- 각 replica/connector/device의 안정 ID
Operation identity- 중복 apply 방지를 위한 operation ID와 idempotency key
Origin propagation- outbound에서 inbound까지 origin metadata를 보존하는 규칙
Authority map- entity/field/relation별 owner
Conflict policy- reject, queue, merge, owner-wins, manual-review, CRDT 등
Causality metadata- revision, vector clock, parent operation, base version
Checkpoint policy- durable cursor advance timing
Retention policy- processed operation log, tombstone, alias, replay window
Convergence tests- duplicate delivery, reordered delivery, delayed echo, concurrent edit, delete/update race, owner migration 시나리오
Cautions#
-
현재 실행 환경에는 사용자가 명시한
WebSearch/WebFetch도구가 노출되어 있지 않아, 실제 라이브 웹 검색 및 본문 fetch 검증을 수행하지 못했다. 아래 Sources는 공개적으로 접근 가능한 신뢰 문서 후보를 기반으로 한 private capsule 초안용 출처이며, 캡슐 확정 전 재검증이 필요하다. -
“Echo suppression”이라는 용어와 구현 방식은 제품마다 다르다. 어떤 시스템은 operation ID, 어떤 시스템은 transaction ID, 어떤 시스템은 delta token/checkpoint, 어떤 시스템은 replication history를 사용한다. 특정 제품의 내부 구현이 동일하다고 단정하면 안 된다.
-
origin_node_id만으로 echo를 무조건 버리면 안 된다. 같은 origin에서 발생한 별도 사용자 변경, retry된 command, delayed webhook, backfill 이벤트가 섞일 수 있다. echo suppression은 origin뿐 아니라 operation ID, entity revision, causal metadata를 함께 봐야 한다. -
Vector clock, version vector, CRDT는 강력하지만 비용이 있다. 저장 메타데이터 증가, pruning, UX 충돌 표시, schema migration, cross-system mapping 문제가 생긴다. 단일 authoritative server와 optimistic concurrency로 충분한 시스템에 과도하게 적용할 필요는 없다.
-
Last-write-wins는 수렴성을 주는 간단한 정책일 수 있지만, 무손실 병합 정책은 아니다. 특히 client timestamp 기반 LWW는 clock skew와 offline edit에 취약하다.
-
Field-level ownership은 충돌을 줄이지만, owner 변경 자체가 새로운 migration 문제를 만든다. 예를 들어 billing system에서 CRM으로
subscription_status의 authority를 이전하는 경우, 전환 기간의 dual-write와 stale event 처리가 별도 계약으로 필요하다. -
공개 문서들은 CouchDB, Dynamo/Riak, Microsoft Graph, CRDT 논문 등 서로 다른 맥락의 시스템을 설명한다. 이 초안은 공통 아키텍처 패턴을 추출한 것이며, 특정 시스템의 설계를 모든 bidirectional sync engine에 그대로 일반화해서는 안 된다.
Sources#
- https://docs.couchdb.org/en/stable/replication/protocol.html
- https://docs.couchdb.org/en/stable/replication/conflicts.html
- https://learn.microsoft.com/en-us/graph/delta-query-overview
- https://learn.microsoft.com/en-us/azure/architecture/patterns/leader-election
- https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
- https://riak.com/why-vector-clocks-are-easy/
- https://hal.inria.fr/inria-00555588/document
- https://cloudevents.io/
- https://datatracker.ietf.org/doc/html/rfc9110
- https://www.w3.org/TR/activitystreams-core/
Related#
Sagwan Revalidation 2026-07-18T21:12:54Z#
- verdict:
ok - note: 양방향 동기화의 소유권·echo 억제·수렴 계약 원칙은 여전히 유효함
Sagwan Revalidation 2026-07-20T21:41:38Z#
- verdict:
ok - note: 원칙 중심 내용으로 최신 동기화 설계 관행과 충돌하지 않는다.