구글 드라이브식 파일 동기화 — Dropbox의 실제 구현
파일 저장 한 번이 다른 기기에 나타나기까지 — 세 트리(Remote/Local/Synced) 비교, 4MB 블록 + SHA-256 blocklist의 delta sync, SFJ commit, longpoll → cursor list → retrieve_batch, 그리고 conflicted copy까지 22단계로 따라간다.
단계 목록 · 키보드 ←/→ 이동, Space 재생
출처: Dropbox — Streaming File Synchronization · Dropbox — Rewriting the heart of our sync engine (Nucleus) · Dropbox — Inside the Magic Pocket
Dropbox 동기화는 '파일 복사'가 아니라 네 장치의 조합이다 — 세 트리 비교(변경 출처 판정), content-addressed 4MB 블록(바뀐 조각만 전송), SFJ + cursor(변경 이력 추적), conflicted copy(충돌은 병합 대신 양쪽 보존).
왜 헷갈리는가
'1GB 파일에서 한 글자 바꾸면 1GB를 다시 올리겠지'가 흔한 오해다. 파일은 최대 4MB 블록으로 잘려 SHA-256 해시로 주소화되고, commit 때 서버가 '없는 블록'만 알려주므로 실제 전송은 바뀐 블록 몇 개뿐이다(delta sync + dedup). 또 하나 — 동시 수정은 '나중 저장이 덮어쓴다'가 아니다. 먼저 commit된 쪽이 원본이 되고, 나중 쪽은 conflicted copy라는 별도 파일로 보존된다.
애니메이션이 보여주는 것
기기 A의 저장 한 번에서 시작한다. OS 파일시스템 이벤트가 Local Tree를 갱신하면, Planner가 세 트리(Remote/Local/Synced)를 비교해 '로컬 변경'을 판정하고 업로드 작업을 산출한다. 20MB 파일이 5개의 4MB 블록으로 잘리고, blocklist commit에서 서버가 '없는 블록 1개'만 요구하는 dedup 장면이 이 시나리오의 핵심 컷이다.
후반부는 세 가지 비정상 경로다 — 업로드가 3/5 블록에서 끊겼다가 해시 기준으로 재개되는 장면(절반 반영 없음), 두 기기가 같은 파일을 고쳐 conflicted copy가 생기는 장면, 그리고 문단 하나 고칠 때마다 반복되는 정상 사이클.
세 트리 — 동기화 엔진의 진짜 심장
Nucleus(2020년 Rust로 재작성된 동기화 엔진)는 세 개의 트리를 유지한다. Remote Tree는 클라우드가 아는 상태, Local Tree는 디스크에서 관찰한 상태, Synced Tree는 마지막으로 동기화가 완료된 상태다. Synced Tree가 버전 관리의 merge base에 해당한다 — Local만 Synced에서 멀어졌으면 업로드, Remote만 멀어졌으면 다운로드, 둘 다 멀어졌으면 충돌이다.
이 판정이 순수한 트리 비교라서 결정론적이다. Dropbox는 CanopyCheck(무작위 트리 조합에 불변식 검증)와 Trinity(장애 주입 시뮬레이션)로 매일 수백만 시나리오를 돌려 이 로직을 검증한다.
- Local ≠ Synced, Remote = Synced → 업로드
- Remote ≠ Synced, Local = Synced → 다운로드
- 둘 다 ≠ Synced → 충돌: 먼저 commit이 원본, 나중은 conflicted copy
- 세 트리 일치 → 할 일 없음(동기화 완료 상태)
왜 메타데이터와 블록을 분리하는가
Metadata 서버는 namespace와 Server File Journal(SFJ)을 관리한다 — 작은 레코드의 트랜잭션 처리다. Block 서버와 그 뒤의 Magic Pocket은 SHA-256으로 주소화된 불변 블록의 대용량 저장이다. 요구 특성이 정반대라 계층을 가른다. commit은 blocklist의 블록이 전부 존재해야 성공하므로 '절반 반영된 파일'은 프로토콜상 존재하지 않고, 업로드가 끊겨도 이미 올라간 블록은 해시로 확인해 건너뛴다.
전파는 push가 아니라 통지 후 pull이다. SFJ에 새 행이 append되면 Notification 서버가 longpoll로 대기 중인 기기를 깨우고, 기기는 자기 cursor(Journal ID)를 담아 list를 요청해 그 이후 행들을 받은 뒤, 없는 블록만 retrieve_batch로 내려받아 임시 파일에 조립하고 원자적으로 교체한다. Magic Pocket 안에서는 Block Index(샤딩 MySQL)가 해시→위치를 기록하고, 존 간 복제 데몬이 약 1초 안에 다른 존으로 사본을 만들며, 뜸해진 데이터는 erasure coding으로 재인코딩된다.
기억할 것
- 세 트리(Remote/Local/Synced) 비교가 변경의 출처를 판정한다 — Synced Tree가 merge base다.
- 파일은 4MB 블록 + SHA-256 blocklist로 content-addressed — 바뀐 블록만 전송·저장한다(delta sync + dedup).
- commit은 SFJ에 행이 append되는 순간이고, 블록이 전부 존재해야 성공한다 — 절반 반영 없음, 중단 후 재개 가능.
- 전파는 longpoll 통지 → cursor 기반 list → 부족 블록 retrieve_batch — 통지를 놓쳐도 cursor가 따라잡는다.
- 동시 수정은 병합하지 않는다 — 먼저 commit이 원본, 나중 쪽은 conflicted copy로 무손실 보존.