Zettelkasten

Durable Objects는 상태마다 전 세계 유일한 단일 스레드 인스턴스를 두어 락 없이 동시성을 푼다

·수정 1

요약

  • Durable Objects(DO)는 "무상태 서버 + 거대 공유 DB" 대신 "상태마다 전 세계에 유일한 단일 스레드 인스턴스"를 두는 모델이다.
  • 동시성 안전은 저장소 분리가 아니라 Global Uniqueness(인스턴스 하나뿐) + 직렬 처리에서 나온다 — 락·분산 합의가 원천 불필요.
  • 대가는 단일 스레드의 처리량 병목이고, 상태를 논리적 단위로 샤딩해 분산한다.

본문

푸는 문제

일반 stateless 서버는 상태를 외부 DB/Redis에 두고 여러 인스턴스가 나눠 본다. 이 구조의 근본 고통 세 가지:

  1. 상태 접근마다 외부 네트워크 왕복 → 느림, DB가 병목
  2. 여러 인스턴스가 같은 데이터를 동시에 건드림 → race condition → 락·트랜잭션·분산 합의 필요
  3. 관련된 클라이언트(채팅방·매칭 큐)가 서로 다른 인스턴스에 흩어져 붙음 → 실시간 협력에 별도 pub/sub 배선

DO의 발상 전환: 상태를 앱의 논리적 단위로 쪼개고, 각 단위마다 전담 인스턴스를 하나씩 둔다. 채팅방 하나 = 인스턴스 하나, 매칭 풀 하나 = 인스턴스 하나.

동시성이 사라지는 진짜 이유

흔한 오해: "객체마다 저장소가 따로 있어서 충돌이 안 난다"는 틀리다. 저장소가 분리돼도 그 하나의 저장소에 동시 요청이 몰리면 충돌한다. 실제 메커니즘은 둘:

  • Global Uniqueness: 같은 클래스+ID 인스턴스는 전 세계에 동시에 단 하나만 실행된다. "두 서버가 동시에 같은 유저를 매칭"하는 상황 자체가 성립 불가. 유일성은 이벤트 수신·스토리지 접근 시점에 강제된다.
  • 단일 스레드 직렬 처리: 그 하나의 인스턴스도 요청을 한 번에 하나씩 순서대로 처리. 작업이 끝나기 전 다음 요청이 끼어들 수 없다.

→ "창구가 하나뿐 + 한 사람씩 줄 서서" 처리되므로 락 없이 동시성 안전.

대가와 샤딩

단일 스레드라 처리량(throughput)에 상한이 있다 — 서버를 늘려도 그 객체 하나의 처리 속도 이상으론 못 올린다. 그래서 상태를 지역/속성 기준으로 여러 객체로 샤딩해 부하를 분산한다(객체끼리 독립이라 객체 수만큼 병렬). 단, 너무 잘게 쪼개면 객체 간 협력(cross-object) 문제가 생겨 균형이 설계 포인트.

기술적 기반 (검증됨)

  • 컴퓨트: Cloudflare Workers와 동일한 V8 Isolate. 컨테이너/VM이 아닌 경량 샌드박스 → 싸고 빠르게 수많은 인스턴스 생성.
  • 스토리지: SQLite를 애플리케이션과 같은 스레드에 라이브러리로 임베드(GA 기준 객체당 10GB). 네트워크 왕복 없이 동기 쿼리 가능.
  • 내구성: Storage Relay Service가 트랜잭션마다 변경 로그를 5개 follower에 전파하고 3개 확인 시 쓰기 확정(3/5 쿼럼) + 30일 PITR.
  • 타이머: Alarm API로 객체가 스스로 "N초 뒤 깨워줘"를 예약 → 외부 크론 없이 타임아웃 처리. 상태와 타이머가 한 몸인 게 강점.

계보: Actor 모델

각 객체 = 고유 주소를 가진 독립 단위 + private 상태 + 메시지 직렬 처리. Erlang/Akka/MS Orleans의 actor 모델, 특히 Orleans의 "virtual actor(필요 시 깨고 안 쓰면 잠듦)"와 가장 가깝다. Cloudflare는 혼동을 피해 "actor"라 부르진 않지만 본질은 엣지 규모로 구현한 가상 actor다. 2020년 Serverless Week에 Kenton Varda 주도로 발표.

한 문장 본질

분산 환경에서 상태의 일관성과 동시성 조율을 락·합의 알고리즘 없이, 상태를 논리 단위로 쪼개 각 단위마다 전 세계 유일 인스턴스를 두고 직렬 처리함으로써 푸는 기술.

관련 노트

참고