SiliconValley_survivor

SiliconValley_survivor

Design Dropbox, 파일 동기화

오프라인에서 따로 수정한 파일을 보존하면서 여러 기기의 파일 이름, 위치, 내용이 일관된 상태로 수렴하도록 만드는 시스템

SiliconValley_survivor's avatar
SiliconValley_survivor
Sep 29, 2026
∙ Paid

Introduction & Requirements

노트북에서 제안서를 고치고 덮개를 닫았습니다. 집 데스크톱에서는 같은 파일의 예전 버전을 열어 다른 문단을 고치고 있었고, 그사이 동료는 공유 폴더 이름을 바꾸고 다른 동료는 그 안 파일을 지웠습니다.

각 기기에서는 모두 정상적인 파일 작업입니다. 서버가 막아야 하는 실패는 장애가 아니라, 서로의 변경을 아직 모르는 채 쌓인 정상 편집을 설명 없이 덮어쓰는 일입니다. 사흘 뒤 노트북이 다시 붙어도 사용자가 쓴 문장은 사라져서는 안 됩니다.

가장 단순한 구현은 폴더를 주기적으로 훑고 수정 시각이 바뀐 파일 전체를 올리는 방식입니다. 작은 파일 몇 개, 사용자 한 명이면 이걸로도 시작합니다.

파일이 늘면 목록 조회와 디스크 탐색 비용이 먼저 커집니다. 큰 파일을 한 줄만 고쳐도 전체를 다시 보내면 대역폭이 먼저 무너지고, 여러 기기가 같은 파일을 고치면 수정 시각만으로는 어느 쪽을 살릴지 결정할 수 없습니다. 기기 시계가 맞다는 보장도 없습니다.

이 제품에서 오프라인은 예외 장애가 아닙니다. 서버와 클라이언트가 둘 다 정상인데 서로의 변경을 모르는 상태가 기본 사용 방식에 가깝습니다.

Dropbox 공개 도움말도 동시 편집, 오프라인 편집, 자동 저장 상황에서 충돌 사본이 생길 수 있다고 말합니다. 일반 파일의 서로 다른 내용을 항상 자동 병합해 주는 제품으로 이해하면 설계가 빗나갑니다.

공개된 사실과 이 글의 설계 범위

2026년 9월에 확인한 Dropbox 공식 아키텍처 설명은 파일 메타데이터와 파일 본문 저장을 분리합니다. 메타데이터에는 MySQL 기반 서비스를 사용한다고 설명하며, 파일 본문은 암호화된 블록으로 저장합니다. 변경 알림은 클라이언트가 메타데이터를 다시 조회하도록 깨우는 역할을 합니다.

Dropbox가 2020년에 공개한 Nucleus 글은 안정적인 파일 식별자와 명확한 상태 모델의 필요성을 설명합니다. 같은 해 공개한 테스트 글에는 로컬 상태, 원격 상태, 동기화 기준 상태를 나타내는 세 트리가 등장합니다. 이는 당시 공개된 설계이며, 현재 모든 플랫폼의 내부 구현이 그대로라고 단정하지 않겠습니다.

2026년 4월에 공개된 Magic Pocket 글에서는 불변 데이터의 삭제와 실제 디스크 공간 회수를 구분합니다. 사용하지 않는 데이터를 찾아내는 작업과 살아 있는 데이터를 다시 모아 저장 공간을 회수하는 작업이 별도로 필요하다는 설명입니다.

아래의 PostgreSQL 스키마, API, 청크 크기, 트래픽, 보존 기간, 리전 구성은 공개 제품 동작을 만족시키기 위한 설계 가정입니다. Dropbox의 실제 내부 프로토콜을 재현한 명세가 아닙니다.

Functional Requirements

  • 사용자는 로컬 폴더에서 파일을 생성하고 수정하며 이름과 위치를 바꿀 수 있습니다.

  • 시스템은 접근 권한이 있는 다른 기기에 변경된 메타데이터와 필요한 파일 내용을 전달합니다.

  • 사용자는 오프라인에서 파일을 수정할 수 있으며, 다시 연결되면 미전송 변경이 동기화됩니다.

  • 시스템은 동시 수정이 발생하면 서로 다른 내용을 보존하고 충돌을 사용자에게 보여줍니다.

  • 사용자는 공유 폴더를 여러 사람과 함께 사용하며, 권한 변경은 이후의 읽기와 쓰기에 적용됩니다.

  • 시스템은 삭제 이력과 이전 버전을 정해진 기간 동안 보존하고 복원을 지원합니다.

  • 사용자는 폴더별 동기화 범위와 파일의 로컬 보관 여부를 선택할 수 있습니다.

  • 시스템은 대용량 전송을 재개하고, 파일별 대기 상태와 진행 상황을 보여줍니다.

Non-Functional Requirements

아래 수치는 모두 설계를 위한 가정이며 Dropbox의 공개 SLA가 아닙니다.

  • 메타데이터 조회는 서버 처리 시간 기준 p99 200ms, 본문 업로드가 끝난 뒤 버전 확정은 p99 500ms를 목표로 합니다.

  • 연결된 정상 기기에서 작은 파일의 서버 확정 이후 로컬 적용까지 p95 5초를 목표로 합니다. 파일 크기, 회선 상태, 로컬 디스크 문제는 별도 구간으로 측정합니다.

  • 메타데이터 API의 월간 가용성 목표를 99.95%로 둡니다. 본문 전송 성공률과 동기화 지연은 독립적인 지표로 관리합니다.

  • 성공 응답을 반환한 버전은 메타데이터와 본문 모두 약속된 장애 범위에서 복구 가능해야 합니다. 이 글에서는 단일 리전 상실에도 승인된 버전을 보존하는 정책을 가정합니다.

  • 하나의 공유 공간 안에서는 파일 버전 확정, 이름 충돌 검사, 삭제, 권한 변경을 일관된 순서로 처리합니다. 다른 기기에 표시되는 시점은 늦을 수 있습니다.

  • 클라이언트가 재시도해도 같은 변경이 여러 버전이나 여러 충돌 사본으로 늘어나지 않아야 합니다.

  • 파일 내용뿐 아니라 파일명, 경로, 공유 관계도 접근 통제 대상으로 취급합니다.

  • 업로드, 다운로드, 복구, 저장 공간 회수 작업은 서로의 처리 용량을 독점하지 않도록 제한합니다.

Out of Scope

  • 문서 편집기 내부의 문자 단위 실시간 공동 편집과 CRDT 병합

  • 파일 본문 검색, AI 요약, 추천, 미디어 변환 파이프라인

  • 외부 공유 링크의 전체 제품 설계와 결제 시스템

  • 모든 운영체제의 특수 파일, 하드 링크, 심볼릭 링크 의미를 완전히 동일하게 만드는 작업

  • 종단 간 암호화 전용 제품의 키 배포와 복구 설계

전송 및 저장 암호화, 권한 검증, 일반 파일의 충돌 보존은 포함합니다. 파일 동기화를 제공한다고 해서 서로 다른 애플리케이션이 만든 바이너리 파일의 의미까지 이해할 필요는 없습니다.

Traffic & Capacity Assumptions

사용자와 변경 요청

  • 하루 활성 사용자 1,000만 명, 사용자당 연결 기기 평균 2대를 가정하겠습니다. 한 사용자가 하루 40번의 파일 생성, 내용 수정, 이동, 삭제를 서버에 확정한다면 하루 4억 건의 변경이 발생합니다.

운영체제가 발생시키는 파일 이벤트와 서버에 확정하는 변경은 다릅니다. 편집기의 연속 저장을 짧은 구간에서 합칠 수 있으므로 파일 감지 이벤트 수를 그대로 서버 쓰기 QPS로 사용하지 않습니다.

  • 하루 4억 건을 86,400초로 나누면 평균 약 4,630건/sec입니다. 피크를 평균의 5배로 가정하면 초당 약 2만 3,150건의 변경을 처리해야 합니다.

이 값은 전체 시스템의 합계입니다. 개인 폴더와 공유 폴더의 메타데이터를 여러 DB 샤드에 분산해야 하지만, 특정 대형 공유 폴더 하나에 변경이 집중되는 상황도 따로 측정해야 합니다.

변경 요청 하나는 권한 조회, 세션 검사, 버전 추가, 이벤트 기록 등 여러 DB 연산을 만듭니다. 이 QPS를 그대로 DB 쿼리 수나 디스크 IOPS로 해석하지 않고 실제 트랜잭션당 비용으로 환산합니다.

변경 조회와 알림

  • 확정된 변경마다 평균 2대의 다른 기기가 메타데이터를 받아간다고 가정하면 하루 8억 건의 변경 항목 전달이 발생합니다. 평균 약 9,260개/sec, 같은 피크 배수를 적용하면 약 4만 6,300개/sec입니다.

이 수치는 HTTP 요청 수가 아니라 응답에 포함되는 변경 항목 수입니다. 여러 변경을 한 페이지에 묶으면 요청 수가 줄고, 알림을 합치면 공유 폴더에서 발생하는 중복 깨우기도 줄일 수 있습니다.

  • 동시 연결 기기를 300만 대로 가정하겠습니다. 모든 기기가 30초마다 변경 유무를 확인하면 변경이 없어도 초당 10만 건의 요청이 생깁니다.

따라서 변경 알림용 연결과 실제 변경 조회 경로를 분리합니다. Long polling을 사용하면 유휴 조회를 줄일 수 있지만 열린 연결의 메모리, 타임아웃 재연결, 리전 장애 후 재접속 부하를 수용해야 합니다.

파일 본문과 저장 공간

  • 전체 변경의 60%가 본문 변경이며, 그때마다 실제로 전송하는 새 데이터가 평균 1MB라고 가정하겠습니다. 하루 약 2억 4,000만 번의 본문 변경과 240TB의 업로드가 발생합니다.

평균 업로드 대역폭은 약 2.78GB/sec, 약 22.2Gbps입니다. 피크가 5배라면 약 111Gbps이며, TLS, 재전송, 내부 복제 트래픽은 별도입니다. 메타데이터 API 서버가 이 본문을 모두 중계하도록 만들면 API 확장과 네트워크 확장이 서로 묶입니다.

  • 새 본문을 다른 기기 평균 2대가 내려받는다면 다운로드는 하루 약 480TB입니다. 본문 변경 1MB라는 평균에는 대용량 영상과 작은 문서가 섞여 있으므로 파일 크기별 지연 분포도 필요합니다.

온라인 전용 파일은 즉시 내려받지 않아도 됩니다. 이 비율이 높아지면 실제 다운로드량은 줄지만 사용자가 파일을 열 때 발생하는 다운로드 지연이 더 중요해집니다.

  • 변경 이벤트를 인덱스 제외 평균 1KB로 잡으면 하루 약 400GB입니다. 30일의 변경 피드만 보관해도 약 12TB이며, DB 인덱스와 복제본은 추가됩니다.

업로드된 240TB를 매일 새로운 고유 데이터라고 보는 보수적 시나리오에서는 30일에 7.2PB가 추가됩니다. 이는 총 보유 데이터가 아니라 해당 기간의 증가량이며, 버전 보존과 중복 제거 효과를 따로 계산해야 합니다. 저장 공간 회수 속도가 유입 속도를 따라가지 못하는 상황도 용량 계획에 포함합니다.

Core Entities & Data Modeling

파일 본문보다 먼저 결정해야 할 것은 같은 파일을 무엇으로 식별하고 어떤 변경을 원자적으로 처리할지입니다.

`/Projects/Proposal.pdf`라는 경로는 파일의 위치입니다. 폴더 이름이 바뀌면 경로도 달라집니다. 파일 자체는 `node_id`로 식별하고 이름과 부모 폴더를 별도 속성으로 관리합니다.

Dropbox의 공개 API도 파일 ID, 경로, 특정 버전을 나타내는 `rev`를 구분합니다. 파일 ID는 이동이나 이름 변경 후에도 유지되는 참조로 설명되어 있습니다. 여기에서도 이 구분을 사용하되 아래 필드명과 버전 정책은 자체 설계입니다.

공유 공간을 트랜잭션 경계로 사용하기

개인 루트 폴더나 하나의 공유 폴더처럼 접근 권한과 변경 순서를 함께 관리하는 공간을 namespace라고 부르겠습니다. 한 사용자가 여러 공유 폴더에 참여하면 여러 namespace를 구독합니다.

같은 namespace 안에서는 이름 중복 검사, 이동, 버전 확정, 변경 이력 추가를 하나의 DB 트랜잭션으로 처리합니다. 같은 공유 폴더를 사용자별로 복제해 저장하지 않으므로 참여자마다 서로 다른 원본을 만들지 않습니다.

다른 namespace로 옮기는 작업은 별도입니다. 이 글에서는 복사 대상 확정 후 원본을 조건부 삭제하는 작업으로 정의하며, 여러 공간을 가로지르는 원자적 이동을 약속하지 않습니다.

구분해서 보관할 식별자

`namespace_id` , 권한과 변경 순서를 관리하는 공유 공간

`node_id` , 이동하거나 이름이 바뀌어도 유지되는 파일 또는 폴더

`revision_id` , 확정된 파일 내용의 불변 버전

`node_version` , 내용, 이름, 부모, 삭제 상태 변경을 포함하는 메타데이터 버전

`base_revision_id` , 클라이언트가 편집을 시작할 때 알고 있던 내용 버전

`mutation_id` , 기기가 만든 하나의 논리적 변경 요청

`upload_session_id` , 중단된 본문 전송을 이어가기 위한 세션

`namespace_seq` , namespace 안에서 확정된 변경의 순번

`cursor` , 어느 구독 범위의 변경을 어디까지 받았는지 나타내는 불투명 토큰

`manifest_id` , 파일을 구성하는 청크의 순서와 길이가 담긴 불변 목록

내용 수정에는 `base_revision_id`를 사용하고, 이동과 삭제에는 `node_version`을 사용합니다. 다른 기기가 파일 이름만 바꿨다면 내용 수정까지 무조건 충돌시키지 않을 수 있습니다. 반대로 삭제는 사용자가 확인한 이후 발생한 내용 수정도 감지해야 하므로 더 넓은 버전 조건이 필요합니다.

저장 위치를 선택하는 이유

RDBMS에는 파일과 폴더의 원본 상태, 현재 버전 참조, 접근 권한, 업로드 확정 상태, 멱등성 기록, 변경 피드를 둡니다. 이름의 고유 제약과 조건부 버전 변경이 같은 트랜잭션에 들어가야 하기 때문입니다.

파일 본문과 manifest는 불변 객체로 Object Storage에 둡니다. 메타데이터가 갱신될 때 본문을 덮어쓰지 않으므로 이전 버전 복원과 다운로드 중 버전 고정이 쉬워집니다. 대신 더 이상 참조하지 않는 객체를 안전하게 회수해야 합니다.

Event Stream에는 확정된 변경을 알림, 검색 인덱싱, 감사 분석으로 전달합니다. 스트림이 지연되어도 현재 파일 상태와 복구 가능한 변경 피드는 DB에 남아 있어야 합니다.

Cache에는 특정 버전의 manifest, 디렉터리 조회 결과, 자주 읽는 메타데이터를 둡니다. 파일 내용을 내려받을 권한은 캐시된 경로 하나만 보고 결정하지 않습니다.

Search Index는 파일명 검색이나 빠른 탐색을 위한 파생 데이터입니다. 검색 결과가 늦게 갱신되어도 다운로드 시점에는 현재 접근 권한을 다시 확인합니다. 이번 설계에는 Vector Index나 Online Feature Store가 필요하지 않습니다.

Analytics 저장소에는 전송량, 실패 원인, 클라이언트 버전별 지연 집계를 둡니다. 고객 파일의 본문이나 전체 경로를 운영 분석 이벤트에 불필요하게 복사하지 않습니다.

원본 상태의 스키마

다음은 PostgreSQL용 예시입니다. 우선 일반 파일과 폴더의 상태, 내용 버전, 변경 피드를 보여줍니다. ACL, 세션, 요청 결과 테이블은 뒤에서 책임을 설명합니다.

CREATE TABLE namespaces (
    namespace_id uuid PRIMARY KEY,
    last_seq bigint NOT NULL DEFAULT 0 CHECK (last_seq >= 0),
    acl_version bigint NOT NULL DEFAULT 1,
    created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE nodes (
    namespace_id uuid NOT NULL REFERENCES namespaces(namespace_id),
    node_id uuid NOT NULL,
    parent_id uuid,
    name text NOT NULL,
    name_key text COLLATE "C" NOT NULL,
    kind text NOT NULL CHECK (kind IN ('file', 'folder')),
    status text NOT NULL CHECK (status IN ('active', 'deleted')),
    node_version bigint NOT NULL DEFAULT 1 CHECK (node_version > 0),
    head_revision_id uuid,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (namespace_id, node_id),
    FOREIGN KEY (namespace_id, parent_id)
        REFERENCES nodes(namespace_id, node_id),
    CHECK (parent_id IS NULL OR parent_id <> node_id),
    CHECK (kind = 'file' OR head_revision_id IS NULL)
);

CREATE UNIQUE INDEX nodes_live_name
ON nodes(namespace_id, parent_id, name_key)
WHERE status = 'active' AND parent_id IS NOT NULL;

CREATE UNIQUE INDEX nodes_one_root
ON nodes(namespace_id)
WHERE parent_id IS NULL;

CREATE TABLE file_revisions (
    namespace_id uuid NOT NULL,
    node_id uuid NOT NULL,
    revision_id uuid NOT NULL,
    manifest_id text NOT NULL,
    byte_length bigint NOT NULL CHECK (byte_length >= 0),
    device_id uuid NOT NULL,
    idempotency_key uuid NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (namespace_id, node_id, revision_id),
    UNIQUE (namespace_id, device_id, idempotency_key),
    FOREIGN KEY (namespace_id, node_id)
        REFERENCES nodes(namespace_id, node_id)
);

ALTER TABLE nodes
ADD CONSTRAINT nodes_head_revision_fk
FOREIGN KEY (namespace_id, node_id, head_revision_id)
REFERENCES file_revisions(namespace_id, node_id, revision_id)
DEFERRABLE INITIALLY DEFERRED;

CREATE TABLE namespace_changes (
    namespace_id uuid NOT NULL REFERENCES namespaces(namespace_id),
    seq bigint NOT NULL,
    event_id uuid NOT NULL,
    aggregate_id uuid NOT NULL,
    event_type text NOT NULL,
    device_id uuid NOT NULL,
    idempotency_key uuid NOT NULL,
    payload jsonb NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now(),
    published_at timestamptz,
    PRIMARY KEY (namespace_id, seq),
    UNIQUE (event_id),
    UNIQUE (namespace_id, aggregate_id, event_type,
            device_id, idempotency_key)
);

`name_key`는 서버의 이름 비교 규칙으로 정규화한 값입니다. 운영체제별 대소문자 처리나 Unicode 정규화가 다를 수 있으므로 단순히 DB의 기본 collation에 맡기지 않습니다. 원래 표시 이름은 `name`에 남깁니다.

부모 외래 키는 같은 namespace 안에 부모가 존재하는지만 보장합니다. 부모가 활성 폴더인지, 조상 중 삭제된 폴더가 없는지, 이동 후 순환이 생기지 않는지는 트랜잭션 안에서 추가로 검사합니다. 루트는 서버가 생성하고 일반 API로 이동하거나 삭제하지 못하게 합니다.

`created_at`은 처음 만들어진 시점이고 `updated_at`은 서버가 메타데이터 변경을 승인한 시점입니다. 이 값으로 충돌 승자를 정하지 않습니다. 클라이언트 파일의 수정 시간은 별도 속성으로 보관할 수 있지만 동시성 제어에는 버전 조건을 사용합니다.

`status`는 삭제 여부를 넘어 복원과 접근 가능 상태를 설명해야 하므로 의미가 드러나는 값으로 둡니다. 전송 진행 상태는 이 필드에 섞지 않고 업로드 세션에서 관리합니다.

RDBMS에서 제외할 데이터

파일 본문을 OLTP 테이블의 큰 바이너리 컬럼에 넣지 않습니다. 본문 전송량이 DB 복제와 백업을 키우고, 작은 메타데이터 변경까지 큰 데이터 경로의 영향을 받게 됩니다. 본문은 Object Storage에 두고 DB에는 manifest 참조를 저장합니다.

전송된 바이트 수를 매 패킷마다 DB에 기록하지 않습니다. 클라이언트는 로컬 진행 상황을 관리하고 서버는 청크 수신 영수증이나 제한된 주기의 체크포인트를 남깁니다. 연결별 순간 진행률은 메모리에 둘 수 있지만 재시작에 필요한 완료 청크 목록은 내구성 있게 보관합니다.

운영체제의 모든 파일 감지 이벤트도 서버 DB에 저장하지 않습니다. 클라이언트의 로컬 인덱스와 변경 큐에서 합치고, 안정적인 파일 내용이나 메타데이터 변경이 준비되었을 때 서버에 제출합니다.

청크 해시 목록이 매우 긴 파일은 manifest 객체에 저장합니다. DB에는 식별자와 총 길이, 청크 수 등 검증에 필요한 요약만 둡니다. 매 파일 조회에서 수천 개의 청크 행을 읽는 비용을 피할 수 있습니다.

원시 성능 로그와 연결 heartbeat는 관측 파이프라인으로 보냅니다. 다만 파일 상태의 변경 피드는 복구에 쓰이는 원본 기록이므로 일반 로그와 달리 메타데이터 트랜잭션에 포함합니다.

API Signatures

아래 경로와 JSON은 이 글에서 설계한 API입니다. Dropbox 공개 API의 실제 endpoint와 혼동하지 않도록 자체 버전 경로를 사용하겠습니다.

업로드 세션 생성과 재개

POST /v1/namespaces/ns_42/upload-sessions
Authorization: Bearer <token>
Idempotency-Key: upload_01
Content-Type: application/json

{
  "node_id": "file_91",
  "base_revision_id": "rev_7",
  "byte_length": 10485760,
  "manifest_digest": "sha256:<manifest-digest>",
  "chunking_version": "fixed-4mib-v1"
}
{
  "upload_session_id": "up_38",
  "state": "receiving",
  "expires_at": "2026-09-29T21:00:00Z",
  "chunk_plan_url": "/v1/upload-sessions/up_38/chunks"
}

예제 ID는 읽기 쉽게 축약했습니다. 스키마에 대응하는 실제 API에서는 UUID를 사용합니다.

클라이언트는 청크 목록을 페이지로 제출하고, 서버가 발급한 업로드 권한으로 필요한 청크만 전송합니다. 해시를 알고 있다는 이유만으로 다른 사용자의 청크를 재사용하거나 다운로드할 수는 없습니다. 재사용 검사는 권한이 확인된 범위 안에서 수행합니다.

GET /v1/upload-sessions/up_38
Authorization: Bearer <token>
{
  "state": "receiving",
  "verified_chunk_indexes": [0, 2],
  "missing_chunk_indexes": [1],
  "manifest_sealed": false
}

이 API는 서버가 검증한 진행 상황을 반환합니다. 업로드 응답이 유실되었다고 파일 전체를 다시 보내지 않고 빠진 청크만 재전송합니다. 같은 청크 위치에 다른 해시를 넣으려면 기존 세션을 바꾸는 대신 새 manifest와 세션을 만들어야 합니다.

버전 확정

POST /v1/namespaces/ns_42/files/file_91/commits
Authorization: Bearer <token>
Idempotency-Key: mutation_72
Content-Type: application/json

{
  "device_id": "device_a",
  "base_revision_id": "rev_7",
  "upload_session_id": "up_38",
  "conflict_policy": "preserve_both"
}
{
  "outcome": "committed",
  "mutation_id": "mutation_72",
  "node_id": "file_91",
  "revision_id": "rev_8",
  "node_version": 12,
  "namespace_seq": 1204
}

서버는 청크 검증과 내구성 조건이 충족된 manifest만 확정합니다. 모든 기기의 다운로드 완료나 알림 전달까지 기다리지는 않습니다.

기준 버전이 이미 바뀌었다면 `preserve_both` 정책으로 별도 파일을 생성할 수 있습니다. 이때 `outcome`은 `conflict_copy_created`이며 새 `node_id`와 원본의 현재 버전을 반환합니다. 동일한 `mutation_id` 재시도는 같은 사본을 돌려주어야 합니다.

권한이 사라졌거나 원본의 상위 폴더가 삭제되었다면 공유 공간에 임의로 파일을 만들지 않습니다. `permission_revoked`나 `parent_deleted`를 반환하고 클라이언트는 미전송 내용을 로컬 복구 영역에 보존합니다.

이동과 삭제

POST /v1/namespaces/ns_42/nodes/file_91/move
Authorization: Bearer <token>
Idempotency-Key: mutation_73
Content-Type: application/json

{
  "expected_node_version": 12,
  "destination_parent_id": "folder_8",
  "name": "Proposal-final.pdf"
}
POST /v1/namespaces/ns_42/nodes/file_91/delete
Authorization: Bearer <token>
Idempotency-Key: mutation_74
Content-Type: application/json

{
  "expected_node_version": 13
}

이동은 부모와 이름을 바꾸는 메타데이터 트랜잭션입니다. 파일 본문을 다시 업로드하지 않습니다. 삭제는 복원 가능한 삭제 기록을 남기며, 실제 청크 제거는 비동기로 진행합니다.

이미 다른 변경이 승인되어 버전이 다르면 `409 version_conflict`를 반환합니다. 클라이언트가 버전 값을 최신 값으로 바꿔 무조건 재시도해서는 안 됩니다. 사용자의 원래 의도를 최신 트리와 비교하고 작업을 다시 계획해야 합니다.

변경 피드와 완료 확인

GET /v1/namespaces/ns_42/changes?cursor=<opaque-token>&limit=500
Authorization: Bearer <token>
{
  "entries": [
    {
      "seq": 1204,
      "event_id": "event_55",
      "type": "file_revision_committed",
      "node_id": "file_91",
      "node_version": 12,
      "revision_id": "rev_8",
      "manifest_id": "manifest_8"
    }
  ],
  "next_cursor": "<opaque-next-token>",
  "has_more": false
}

`has_more`가 참이면 이어서 조회합니다. 토큰에는 namespace, 조회 범위, 권한 세대, 전달 위치가 묶이며 클라이언트가 내용을 해석하거나 수정하지 않습니다.

변경 피드를 받았다는 것과 파일을 로컬에 적용했다는 것은 다릅니다. 클라이언트는 수신 커서와 파일별 적용 상태를 따로 보관합니다. 서버의 요청 결과를 확인하는 `GET /v1/namespaces/{ns}/mutations/{mutation_id}`도 제공해 확정 응답 유실을 복구합니다.

POST /v1/namespaces/ns_42/files/file_91/download-ticket
Authorization: Bearer <token>
Content-Type: application/json

{
  "revision_id": "rev_8"
}

다운로드 권한은 현재 ACL을 확인한 뒤 특정 버전에 묶어 발급합니다. 전송 중 최신 버전이 바뀌어도 하나의 다운로드에서 서로 다른 버전의 청크를 섞지 않습니다.

High-Level Design

파일 본문 전송과 메타데이터 확정

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 실리콘밸리_생존자 · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture