분산 SQLite: 12년간 검증한 5가지 통념
12년간의 분산 SQLite(Raft, Litestream, Turso, D1)를 돌아보며 일관성, 복제, Postgres를 언제 써야 할지 다섯 가지 통념을 검증합니다.

분산 SQLite는 한 개발자의 주말 프로젝트에서 출발해, 대부분의 엔지니어가 눈치채지 못한 사이 벤처 투자를 받는 제품 카테고리가 되었습니다. 2014년 9월, Philip O'Toole은 SQLite 앞에 Raft 로그를 붙여 모든 쓰기를 클러스터 전체에 복제하고, 코드를 rqlite라는 이름으로 GitHub에 공개했습니다. 12년이 지난 지금은 dqlite, Litestream, LiteFS, cr-sqlite, libSQL/Turso, Cloudflare D1 등 하나의 생태계가 되었고, 이 분야에 대해 사람들이 믿는 것 중 상당수는 흥미로운 방식으로 틀렸습니다.
저는 클러스터 데이터베이스를 프로덕션에서 운영하고 그 장애 모드를 수습해 본 사람으로서, 여러분의 고생을 줄여 드리고 싶습니다. 분산 SQLite에 대한 논쟁은 거의 SQLite 자체에 관한 것이 아닙니다. 어떤 질문을 하고 있는지가 핵심입니다. 머신이 죽으면 어떻게 하나? 머신이 죽었는데 복구를 기다릴 수 없다면? 아니면 애초에 머신이 없다면? 12년의 역사를 놓고 다섯 가지 통념을 검증해 보겠습니다.
통념 1: 분산 SQLite는 최근의 아이디어다
복제되는 SQLite에 관한 최근 글쓰기 열풍은 2021년 무렵 Litestream과 “SQLite로 충분하다” 운동이 목소리를 높이면서 시작됐고, Turso와 Cloudflare D1이 투자금과 출시 발표를 들고 등장했습니다. 그래서 누군가 최근에 와서 SQLite를 복제할 수 있다는 걸 영리하게 알아챘다는 인식이 생겼죠. 하지만 Raft 기반 SQLite 시스템이 처음 나온 건 2014년이고, 그게 SQLite 복제의 첫 사례도 아닙니다. Expensify는 2016년 Bedrock을 공개하기 몇 년 전부터 Paxos 계열 프로토콜로 내부에서 이미 복제를 하고 있었고, ActorDB는 비슷한 시기에 모든 Erlang 액터에 Raft로 복제되는 SQLite 데이터베이스를 하나씩 붙여 주었습니다.
이게 공로 문제를 넘어 왜 중요할까요? 서로 다른 필요와 프로토콜을 가진 최소 여덟 개 팀이 독립적으로 이 문제를 들여다보고 같은 구조에 도달했기 때문입니다. SQLite를 저장 엔진으로 두고 복제는 바깥에 얹는 구조죠. 이 방식을 그만둘 이유를 아무도 찾지 못했습니다. 10년에 걸쳐 여러 팀이 독립적으로 같은 결론에 수렴하는 패턴이 보인다면, 그건 유행이 아니라 정말 좋은 아이디어를 보고 있는 겁니다. 잠깐 번졌다가 사라지는 카테고리들과 비교해 보세요. 이건 계속 쌓여 왔습니다.
또 이 점이 중요한 이유는, 초기 설계 결정들이 아직도 구조를 떠받치고 있기 때문입니다. rqlite는 2014년에 CP 시스템을 택했습니다. 쓰기는 리더를 거치고, 과반수의 확인이 필요하며, 파티션으로 고립된 소수 쪽은 쓰기를 거부합니다. 이후 그 입장을 한 번도 바꾸지 않았죠. 이 선택 하나가 HTTP API, 읽기 일관성 수준, 스냅샷 동작까지 결정했습니다. 일관성을 나중에 정하면 되는 설정값으로 취급하는 팀은 대개 후회하게 됩니다. 머릿속의 명세와 실제 구현 사이의 간극에서 분산 시스템 버그가 자라나기 때문입니다.
통념 2: Raft를 SQLite에 얹으면 저렴한 분산 Postgres를 얻는다
목록에서 가장 비싼 통념입니다. 세상에서 가장 많이 배포된 데이터베이스에 합의 알고리즘을 얹으면, 클라이언트-서버 시스템의 운영 부담 없이 분산 DB를 얻는다는 말이 솔깃하게 들리죠. 하지만 실제로 얻는 것은 단일 쓰기 노드 데이터베이스입니다. 모든 쓰기가 과반수 노드와의 네트워크 왕복을 치르고, SQL 표면은 여전히 SQLite의 것입니다. 저장 프로시저도 없고, 타입 시스템은 얇고, 동시성 기능도 제한적이죠.
쓰기 경로가 정직한 비용입니다. 로컬 SQLite 커밋은 fsync 몇 번이면 끝납니다. Raft로 복제된 커밋은 리더가 자기 로그에 추가하고, 엔트리를 팔로워에게 보내고, 과반수가 디스크에 영속화하기를 기다리고, SQLite에 적용한 뒤에야 응답합니다. 건강한 사내망에서는 수십 마이크로초가 아니라 1~2밀리초쯤 걸리고, 리전 간에는 빛의 속도와 디스크 지연을 감수해야 합니다. 설정 저장소, 서비스 디스커버리 메타데이터, 작은 컨트롤 플레인처럼 읽기 위주이고 쓰기 빈도가 낮은 워크로드라면 괜찮고, 사실 rqlite가 노린 틈새가 바로 그곳입니다(rqlite의 출발점은 소량의 관계형 저장소가 필요한 클러스터 시스템이었는데, 그 데이터를 어느 노드에 둘지에 대한 좋은 답이 없었기 때문입니다). 하지만 고부하 OLTP 워크로드라면 잘못된 도구입니다.
그리고 README에는 아무도 적지 않는 운영의 현실이 있습니다. 리더 선출은 여러분이 떠안는 장애 모드입니다. 과반수 투표로 스플릿 브레인은 막을 수 있지만, 3노드 클러스터에서 두 대를 잃으면 모든 쓰기가 거부된다는 뜻이기도 합니다. ‘서버가 죽어서 DB가 다운됐다’를 ‘쿼럼이 죽어서 DB가 다운됐다’로 바꾼 셈이고, 후자는 새벽 3시에 당직자에게 설명하기가 훨씬 어렵습니다. 선택한 시스템에 Jepsen 스타일 테스트를 해 봤는지 생각해 봐야 합니다. Hacker News의 한 댓글러는 rqlite가 Jepsen 테스트를 받았는지 날카롭게 물었는데, 테스트를 받지 않은 시스템에는 보통 숨은 문제가 있기 마련입니다. 같은 토론에서 한 경험 많은 엔지니어는, 요즘 도구로는 실제 Postgres를 운영하는 것이 충분히 쉬워져서 “그냥 데이터베이스 서버를 쓰라”는 말이 다시 정당한 기본값이 되었다고 지적했습니다. 틀린 말이 아닙니다.

통념 3: 복제는 곧 합의다
12년의 역사가 실제로 주는 가장 중요한 통찰이 하나 있다면, ‘복제된 SQLite’는 하나의 설계가 아니라 설계 공간의 세 모서리라는 점입니다. 그리고 그 세 가지는 꽤 의미 있는 순서로 등장했습니다. 이 둘을 혼동하면 백업 문제를 풀려고 Raft 클러스터를 운영하는 상황에 빠지게 됩니다.
- 합의 (CP). rqlite, dqlite. 리더는 하나, 강한 일관성, 실제 클러스터. 답하는 질문: “머신이 죽었는데 복구를 기다릴 수 없다면?” 대가: 쓰기 지연과 쿼럼 운영.
- 스트리밍 (단일 쓰기). Litestream, 이후 LiteFS. 합의가 전혀 없습니다. SQLite WAL을 오브젝트 스토리지로 스트리밍해서 단일 노드에 거의 연속적인 재해 복구를 제공합니다. 답하는 질문: “머신이 죽으면?” 대가: 일부 데이터 손실 가능성의 작은 창, 그리고 초~분 단위의 복구 시간.
- 병합 (AP). cr-sqlite와 로컬 퍼스트 운동. SQLite 확장으로 구현한 CRDT로, 리더 없이 여러 쓰기 노드가 갈라졌다가 병합합니다. 답하는 질문: “중앙 머신이 아예 없다면?” 대가: 최종 일관성과, 반드시 설계해야 하는 병합 의미론.
Ben Johnson이 Litestream을 주장한 논리는 존중할 만합니다. 대부분의 애플리케이션은 한 대의 머신에서 돌아가고, 필요한 건 클러스터가 아니라 머신이 죽었을 때 복구할 방법이기 때문입니다. 첫 번째 질문인 “머신이 죽으면?”은 두 번째 질문인 “복구를 기다릴 수 없다면?”보다 훨씬 흔합니다. 저는 복구 목표 시간(RTO)이 “점심 전에만 되면 됨” 정도인 데이터베이스를 지키려고 3노드 Raft 클러스터를 배포하는 팀을 여럿 봤습니다. 그건 신뢰성 엔지니어링이 아니라 카고 컬트죠. 재미있게도 Litestream의 WAL 스트리밍 방식은 나중에 rqlite 자체가 v8에서 WAL을 다루는 방식에 영감을 주었습니다. 스트리밍 모서리가 합의 모서리로 피드백된 셈입니다.
그러니 도구를 고르기 전에 질문을 적어 보세요. 말 그대로 적으세요. “노드 X가 죽으면 N초 안에 쓰기를 재개해야 하고, 마지막 M초의 커밋된 데이터는 잃을 수 있다.” M이 몇 초이고 N이 1분 정도라면 Litestream-to-S3가 답이고, 쿼럼 때문에 새벽에 호출받을 일도 없을 겁니다. M이 0이고 N이 몇 초라면 합의의 영역이며, 모든 쓰기마다 그 비용을 치르게 됩니다. 이건 모든 위협 모델링과 같은 원칙입니다. 방어를 사기 전에 실패를 먼저 명명하세요. 재앙에 저항하는 시스템을 설계할 때도, 어떤 재앙에 대비하는지를 먼저 결정하는 것과 같습니다.
통념 4: 관리형 “엣지의 SQLite”는 트레이드오프를 없애 준다
이 카테고리의 돈은 플랫폼으로 흘러갔습니다. Turso는 벤처 투자를 받은 뒤 Supabase에 인수되었고, Cloudflare는 엣지 네트워크 안에 D1을 출시했습니다. 어려운 분산 부분은 이제 다른 누군가가 처리해 준다는 것이 판매 포인트입니다. 때로는 그게 사실입니다. 하지만 때로는 마케팅일 뿐이고, 불편한 진실이 새어 나옵니다. 한 Cloudflare 개발자는 D1이 사실상 SQLite 데이터베이스를 노출하는 단일 Durable Object를 감싼 래퍼이며, 많은 경우 Durable Objects 스토리지를 직접 쓰는 편이 낫다고 공개적으로 인정했습니다. 합의 의미에서의 분산 데이터베이스가 아니라, 플랫폼으로 감싼 단일 SQLite 파일인 셈이죠. 프로덕션에서 D1을 쓰는 사용자들도 그에 걸맞은 신뢰성을 보고하고 있습니다.
관리형 옵션이 나쁘다는 뜻은 아닙니다. 트레이드오프가 사라진 게 아니라, 들여다볼 수 없는 API 뒤로 옮겨졌다는 뜻입니다. rqlite를 직접 운영하면 CP가 무엇인지 정확히 알 수 있습니다. 노드 3개, 리더 1개, 과반수 확인, 파티션된 소수 쪽은 쓰기 거부. 하지만 ‘엣지의 SQLite’를 사면 블랙박스에 같은 질문을 던져야 합니다. 쓰기 노드는 어디에 있는가, 파티션이 생기면 어떻게 되는가, 실제 내구성 윈도우는 얼마인가? 벤더의 답이 모호하다면 장애 회고에서도 명확한 답은 나오지 않을 겁니다.
복제를 서비스로 사는 것이 일관성 문제를 없애 주지는 않습니다. 그 답을 다른 사람의 인프라로 넘길 뿐이고, 그곳은 여러분이 감사할 수 없습니다.
역사의 패턴을 기억해 둘 만합니다. 오픈소스 시스템(rqlite, dqlite, Litestream)이 8년 동안 아이디어를 구현하고 있는 동안 누군가 그것을 팔기 시작했고, 자본은 아이디어를 청구서가 붙는 서비스로 바꾼 쪽으로 흘러갔습니다. 인프라가 보통 걷는 길입니다. 구매가 맞는 선택인 경우도 많지만, 세 모서리 중 실제로 무엇을 얻는지 눈을 뜨고 확인한 뒤 사야 합니다.

통념 5: 강한 일관성은 백업 위에 공짜로 얹는 업그레이드다
사람들은 선택지를 백업(약하지만 저렴함)과 합의(강하지만 비싸고, 엄밀히 말해 더 나은 것)로 나눠 생각합니다. 그렇지 않습니다. Raft를 통한 강한 일관성은 같은 제품의 더 나은 버전이 아니라 전혀 다른 제품이며, 백업에는 없는 지속적인 비용을 부과합니다.
첫 번째 비용은 처리량입니다. 두 방식을 모두 써 본 한 엔지니어는 rqlite가 “한 곳 이상에 저장된다”는 보장을 주지만 단일 노드 데이터베이스보다 훨씬 느리다고 말했습니다. 합의의 네트워크 왕복 때문이고, 이건 사라지지 않습니다. 두 번째는 미묘한 의미론의 문제입니다. SQL 문장을 그대로 복제하면 결정성 문제를 떠안게 됩니다. random()이나 datetime('now') 같은 함수는 계층이 재작성하지 않으면 노드마다 다른 값을 낼 수 있습니다. 그래서 일부 구현은 SQLite SQL의 일부만 지원합니다. WAL 수준에서 복제하면 이 문제는 피하지만, SQLite의 온디스크 포맷과 페이지 레이아웃에 결합됩니다. 어느 계층에서 복제하든 타협 없이는 불가능합니다. 세 번째는 분산 트랜잭션이 정말 어렵다는 점입니다. 한 엔지니어는 트랜잭션 안의 각 문장을 별도의 Raft 명령으로 보내던 설계(대체로 맞지만 느림)에서, 시작 노드가 트랜잭션 전체를 로컬에서 실행하고 필요하면 롤백한 뒤 배치 전체를 하나의 커밋 명령으로 보내는 설계로 진화시킨 과정을 설명했습니다. 이런 교훈은 직접 이 시스템을 운영해 봐야만 얻을 수 있습니다.
쓰기 경로의 차이를 보여 주는 간단한 예시를 rqlite의 HTTP API로 보겠습니다:
# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.
그리고 여러분이 얻지 못한 것도 확인해야 합니다. 바로 읽기 확장성입니다. Raft는 팔로워가 인계받을 수 있도록 로그를 복제할 뿐, 기본적으로 팔로워가 강한 일관성의 읽기를 제공하게 해 주지는 않습니다(rqlite가 읽기 일관성 수준을 제공하는 이유도 바로 이 트레이드오프 때문입니다). ‘분산’이라는 말에 끌려 읽기 용량을 늘리고 싶었다면, 필요한 것은 합의가 아니라 읽기 복제본이었습니다.
Raft 기반 SQLite 12년이 실제로 가르쳐 준 것
이 카테고리의 역사는 인프라 아이디어가 어떻게 성숙하는지를 보여 주는 드문 명확한 증거입니다. 먼저 누군가 구체적인 필요(실제 업무에서 마주친 실제 클러스터링 문제)에서 원칙에 충실한 무언가를 만듭니다(rqlite). 그다음 다른 이들이 자기 패키징 제약에 맞게 다시 만듭니다. Canonical의 dqlite는 Canonical이 자사 제품 안에 데이터베이스가 필요했기 때문에 임베드하는 라이브러리이고, rqlite는 누구의 제품 옆에든 띄워 두는 서버입니다. 그 뒤 누군가 흔한 경우는 원칙적인 해법이 가정하는 것보다 작다고 주장하고(Litestream), 그 주장이 맞았음이 증명되면 분야 전체가 재조정됩니다. 마지막으로 플랫폼이 이를 감싸서 판매합니다. 인프라 일을 충분히 오래 하면 이 영화를 다시 보게 될 겁니다. 지금도 LLM 에이전트들이 같은 방식으로 분산 시스템의 교훈을 다시 배우고 있습니다.
모든 이야기를 마친 뒤 제가 드리는 군더더기 없는 권고입니다. 먼저 단일 노드 SQLite에 WAL을 오브젝트 스토리지로 스트리밍하는 구성으로 시작하세요. 놀라울 만큼 멀리 갈 수 있고, 장애 시나리오가 합의 사후 분석이 아니라 복구 작업으로 끝납니다. 스트리밍으로는 충족할 수 없는 가용성 요구사항을 한 문장으로 말할 수 있을 때에만 Raft 기반 시스템으로 넘어가고, 그에 따르는 쓰기 지연과 쿼럼 운영 비용을 받아들이세요. SQL 표면이나 다중 쓰기 처리량이 필요해지는 순간 Postgres를 고르세요. 이 중 어느 것도 맞지 않는다면, 임베디드 데이터베이스 세계에는 임베디드 그래프 데이터베이스부터 로컬 퍼스트 CRDT 영역까지 다른 선택지도 있습니다.
통념 검증 요약: 분산 SQLite는 새로운 것이 아니고, 저렴한 Postgres도 아니며, 하나의 설계로 묶이지도 않고, 관리형 래퍼로 안전해지지도 않으며, 백업 위에 공짜로 얹는 업그레이드도 아닙니다. 12년 된, 잘 정리된 설계 공간이고 세 개의 모서리가 있으며, 각각은 서로 다른 질문에 맞습니다. 제대로 데어지는 엔지니어들은 질문을 적어 두지 않은 사람들입니다.


