Skip to content
Published on

SQLite 르네상스 2026 — 24살 단일 파일 DB가 어떻게 가장 핫한 인프라가 되었나 (libSQL·Turso·LiteFS·D1·mvSQLite 심층 분석)

Authors

프롤로그 — 24살 데이터베이스가 왜 다시 핫해졌나

2026년의 인프라 스택을 보면 묘한 장면이 펼쳐진다. 한쪽에서는 Postgres 18이 멀티마스터 논리 복제를 다듬고, 다른 한쪽에서는 분산 NewSQL이 글로벌 일관성을 약속한다. 그런데 가장 시끄러운 트렌드는 그 어느 쪽도 아니다 — 2000년 D. Richard Hipp이 한 파일짜리 임베디드 라이브러리로 시작한 SQLite가 다시 무대 한복판에 섰다.

SQLite는 데이터베이스가 아니다. 라이브러리다. 그게 이 모든 부활의 비밀이다.

이상하게 들리지만 다시 생각해 보자. 2026년 인프라의 두 가지 거시적 압력은 (a) 엣지로 가는 컴퓨트(b) 로컬 우선 UX다. 둘 다 답이 같다 — "네트워크 라운드트립을 없애라". 그리고 네트워크를 없애려면 데이터가 코드 옆에 있어야 한다. 코드 옆에 있는 데이터베이스는 무엇인가? 로컬 파일. SQLite는 처음부터 로컬 파일이었다.

이 글은 24살 먹은 단일 파일 DB가 어떻게 2026년의 가장 흥미로운 인프라 카테고리로 부활했는지 해부한다. 정확히 무엇이 새로운지 — libSQL 포크와 Turso의 임베디드 레플리카, Cloudflare D1, 이제는 일몰된 LiteFS, FoundationDB 기반 mvSQLite, Litestream 백업 — 그리고 SQLite를 "엣지 데이터베이스"로 만든 토폴로지가 무엇인지. 그리고 정직하게 말한다 — SQLite가 옳은 선택일 때와 절대 틀린 선택일 때.


1장 · SQLite-at-the-edge 테제 — 왜 지금인가

먼저 명확히 하자. SQLite는 새롭지 않다. 1.0이 2000년에 나왔다. 모든 안드로이드, iOS, macOS, 모든 브라우저, 거의 모든 IoT 기기에 들어 있다. 세계에서 가장 많이 배포된 데이터베이스다. 그런데 왜 2026년에 갑자기 "트렌드"가 되었나?

세 가지가 동시에 일어났다.

첫째, 엣지 컴퓨트가 메인스트림이 됐다. Cloudflare Workers, Deno Deploy, Vercel Edge Functions, Fly.io — 코드는 사용자 가까이로 갔다. 그런데 데이터베이스는? 여전히 us-east-1에 있다. 서울 사용자가 도쿄 엣지에서 실행되는 함수를 호출했는데, 그 함수가 us-east-1의 Postgres로 200ms 라운드트립을 하면, 엣지의 의미가 사라진다.

둘째, 로컬 우선(local-first) 운동이 형태를 갖췄다. Ink & Switch의 2019년 에세이로 시작된 흐름이 2026년에는 실제 제품들 — Linear, Figma, Notion의 일부 — 로 구체화됐다. "오프라인에서도 동작하고, 즉각적이고, 내 데이터는 내 기기에" 라는 약속. 이를 위해서는 데이터가 클라이언트 안에 있어야 한다. 클라이언트에 들어가는 임베디드 SQL DB는? SQLite다.

셋째, "스케일 아웃이 답"이라는 신화가 흔들렸다. 2020년대 초반에는 모든 회사가 "분산 분산 분산"을 외쳤지만, 2026년에 모두가 깨달은 것은 — 대부분의 앱은 분산 일관성이 필요 없다. 하나의 노드에 다 들어간다. 그리고 단일 노드 데이터베이스는 운영이 거짓말처럼 단순하다.

이 세 가지의 교집합이 바로 SQLite다. 로컬 파일이고, 임베디드이고, 작고, 신뢰할 수 있다. 거기에 2020년대 중반의 새 도구들이 SQLite의 약점 — 분산, 복제, 백업 — 을 메우기 시작한 것이 이 르네상스의 본질이다.


2장 · libSQL과 Turso — "임베디드 레플리카"라는 발명

이 부활의 가장 영향력 있는 한 가지를 골라야 한다면 Turso의 임베디드 레플리카(embedded replica) 다.

libSQL — SQLite의 오픈 포크

SQLite 자체는 오픈소스지만 외부 컨트리뷰션을 받지 않는다. SQLite 팀은 "퍼블릭 도메인이지만 우리 코드는 우리가 쓴다"는 입장이다. 이게 갈증을 만들었다 — 사람들은 SQLite에 새 기능 (스토리지 백엔드 교체, 네이티브 복제, 더 나은 동시성) 을 붙이고 싶었지만 길이 없었다.

그래서 2022~2023년경 Turso(당시 ChiselStrike)가 libSQL을 만들었다. SQLite의 진짜 포크다. MIT 라이선스, 외부 컨트리뷰션 받음, 그리고 가장 중요하게 — 네이티브 네트워크 프로토콜(HTTP, WebSocket via "Hrana"), 사용자 정의 함수, 그리고 임베디드 레플리카를 추가했다.

임베디드 레플리카 — 모델 자체가 새롭다

전통적인 모델: 앱 ↔ 네트워크 ↔ DB. 모든 쿼리가 라운드트립.

Turso의 임베디드 레플리카: 앱 안에 SQLite 파일이 있다. 읽기는 그 로컬 파일에서 일어난다 — 마이크로초 단위. 쓰기는 원격 마스터로 보내지고, 마스터가 변경분(WAL 프레임)을 백그라운드로 푸시해 로컬 파일을 동기화한다.

┌────────────────────────┐                    ┌──────────────────┐
│       앱 (예: 엣지)    │                    │   Turso 마스터   │
│  ┌──────────────────┐  │   읽기: 로컬 디스크 │  (글로벌 1차)    │
│  │ libSQL 클라이언트 │──┘   < 1ms          │                  │
│  │  + 로컬 .db 파일  │                       │                  │
│  └────────┬─────────┘                       │                  │
│           │ 쓰기 (HTTP/Hrana) ─────────────▶│                  │
│           │ ◀── WAL 프레임 (백그라운드 sync) │                  │
└────────────────────────┘                    └──────────────────┘

이게 뭐가 그렇게 새로운가? 읽기와 쓰기의 비대칭성을 처음으로 정직하게 받아들였다. 대부분의 웹 앱은 99% 읽기다. 읽기를 1ms 이하로 만들 수 있다면, 쓰기는 라운드트립을 감수해도 된다. 그런데 기존의 "리드 레플리카" 모델은 항상 네트워크 너머에 있었다. Turso의 임베디드 레플리카는 그 레플리카를 앱 프로세스 안의 디스크 파일로 가져왔다.

코드: Turso 임베디드 레플리카 셋업

// pnpm add @libsql/client
import { createClient } from '@libsql/client'

const db = createClient({
  // 로컬 SQLite 파일 (디스크에 진짜로 존재함)
  url: 'file:local.db',
  // 원격 1차 — 쓰기는 여기로
  syncUrl: 'libsql://my-db-myteam.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN,
  // 백그라운드 동기화 주기
  syncInterval: 60, // 초
})

// 첫 sync — 원격 상태를 로컬에 복제
await db.sync()

// 읽기 — 로컬 파일에서 곧장. 네트워크 없음.
const rows = await db.execute('SELECT * FROM posts WHERE published = 1')

// 쓰기 — 원격으로 가고, 다음 sync에 로컬도 따라잡음
await db.execute({
  sql: 'INSERT INTO posts (title, body) VALUES (?, ?)',
  args: ['Hello', 'World'],
})

// 수동 동기화도 가능
await db.sync()

이 모델의 의미는 큰데, 가장 큰 것 — 읽기는 로컬 파일 IO만큼 빠르다. SSD에서 SQLite 인덱스 룩업은 마이크로초 단위다. 어떤 분산 DB의 어떤 캐시도 이걸 못 이긴다. 트레이드오프는 일관성 이다. 로컬 레플리카는 마지막 sync 시점까지의 데이터만 본다. 쓰기 직후의 자기 데이터를 즉시 봐야 한다면 read-your-writes 모드를 켜야 하고 (그 경우 그 한 쿼리만 원격으로 가서 라운드트립 발생), 강한 일관성이 필요한 워크로드는 그냥 원격 모드를 써야 한다.

Turso 플랫폼 자체

Turso는 그 위에 매니지드 서비스를 얹었다. 무료 티어가 후하고 (수억 행 무료), 데이터베이스 브랜치 가 있고 (Git 브랜치처럼 DB를 분기), 멀티 리전 1차도 가능하다. 2024년 이후로 Turso의 큰 베팅은 per-tenant 데이터베이스 다 — 사용자 한 명당 DB 한 개. SQLite는 가볍기 때문에 수십만 개의 작은 DB가 가능하다. 멀티테넌시의 새 모델이다.


3장 · LiteFS — Fly의 분산 SQLite 실험과 그 일몰

Fly.io는 2022년에 다른 답을 시도했다 — LiteFS, FUSE 파일시스템 레이어다.

LiteFS의 아이디어

평범한 SQLite 파일을 FUSE 마운트 위에 두면, LiteFS가 그 파일에 일어나는 모든 트랜잭션(WAL 프레임)을 가로채서 다른 노드에 복제한다. 앱은 자기가 분산 환경에 있는지조차 모른다 — 그냥 SQLite 파일을 연다. 1개 노드가 "1차"이고 모든 쓰기는 1차로 라우팅된다(LiteFS Proxy가 자동으로 처리). 다른 노드들은 거의 실시간으로 복제본을 받는다.

가장 큰 매력은 앱 코드 변경이 없다는 점 이었다. Rails, Django, Phoenix — 어떤 프레임워크든 SQLite를 열 수만 있으면 LiteFS 위에서 분산 모드로 동작했다.

일몰

그런데 2024년 중반, Fly는 LiteFS 개발이 사실상 일시 정지 됐다고 공지했고, 2025년에는 새 사용자에게 더 이상 권장하지 않는다는 신호가 강해졌다(공식 "유지보수 모드(maintenance mode)" 라는 표현이 자주 인용된다). 기술적으로는 FUSE 자체의 복잡성, 멀티 1차의 어려움, 그리고 운영 부담 이 원인으로 거론됐다. Fly의 매니지드 SQLite 비전은 다른 방향(예: Fly Postgres 강화, Tigris 같은 외부 파트너십)으로 이동했다.

그래도 배울 점

LiteFS의 일몰이 아이디어의 실패는 아니다. LiteFS는 "SQLite를 어떻게 분산시킬 것인가" 에 대한 가장 깔끔한 답 중 하나였다. 앱 투명성, 단방향 1차, WAL-레벨 복제. 비록 매니지드 형태로는 살아남지 못했지만, libSQL과 D1이 그 디자인 어휘 — 1차 + 비동기 복제, WAL 프레임 단위 동기화 — 의 일부를 이어받았다.

오늘 분산 SQLite를 새로 시작한다면 LiteFS를 권하지 않는다. 그러나 그 디자인 문서들은 여전히 읽을 가치가 있다.


4장 · Cloudflare D1 — 엣지 SQLite의 매니지드 답

Cloudflare가 던진 답은 다르다. D1은 SQLite 자체를 Workers 런타임 근처에 가져다 놓는다.

D1의 모델

  • 저장 단위: SQLite 데이터베이스.
  • 호스팅: Cloudflare의 글로벌 네트워크 위, 일종의 1차 리전 + 자동 리드 레플리카로 동작.
  • 접근: Workers 바인딩 (env.DB.prepare(...))을 통해서. HTTP API도 있지만 주된 모델은 Workers 내부에서의 호출.
  • 상태: 2026년 기준으로 GA 상태이며, "Global Read Replication" 옵션을 통해 읽기 레플리카가 여러 대륙에 자동 배치된다. 데이터베이스당 크기 한도가 한 자릿수 GB(현재 10GB대) 수준이라 거대 단일 DB보다는 per-application 혹은 per-tenant 모델에 맞는다.

코드 예시: Workers + D1

// wrangler.toml에 D1 바인딩이 있다고 가정
// [[d1_databases]]
// binding = "DB"
// database_name = "blog"
// database_id = "..."

export interface Env {
  DB: D1Database
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const url = new URL(req.url)
    const slug = url.pathname.slice(1)

    // prepare + bind — SQL injection 안전
    const stmt = env.DB.prepare(
      'SELECT title, body, published_at FROM posts WHERE slug = ?1 LIMIT 1'
    ).bind(slug)

    const row = await stmt.first<{ title: string; body: string; published_at: string }>()

    if (!row) return new Response('Not found', { status: 404 })

    return Response.json(row)
  },
}

핵심은 env.DB가 네트워크 핸들이 아니라 런타임이 주입한 바인딩 이라는 점이다. Workers는 D1과 같은 인프라 위에서 돌고, Cloudflare가 라우팅·풀링·복제를 다 숨긴다. 개발자 입장에서는 그냥 "엣지 함수 안에 SQL이 있다".

트레이드오프

  • 쓰기 일관성: 단일 1차에 직렬화. 다중 리전 쓰기는 없다. 글로벌 일관성을 약속하지 않는다.
  • 트랜잭션: 인터랙티브 트랜잭션은 제한적. 배치 API 가 권장된다 — 여러 statement를 한 번에 묶어 보낸다.
  • 크기: DB 1개당 한 자릿수 GB. 더 큰 데이터는 R2 + D1 메타데이터 패턴으로 분할.
  • lock-in: D1은 Workers 생태계 내부에서 가장 빛난다. 다른 곳에서는 매력이 줄어든다.

D1의 진짜 가치는 "Workers를 쓴다면 거의 마찰 없이 영속성을 얻는다" 는 데 있다. SQLite-at-the-edge의 가장 통합적인 구현이다.


5장 · 분산 SQLite의 다른 길 — mvSQLite와 rqlite

위의 셋(libSQL/Turso, LiteFS, D1)이 메인스트림이지만, 더 흥미로운 실험들이 있다.

mvSQLite — FoundationDB로 백킹

mvSQLite는 SQLite의 스토리지 엔진(VFS 레이어)을 통째로 갈아끼웠다. 데이터는 로컬 디스크가 아니라 FoundationDB 에 저장된다. FoundationDB는 Apple이 쓰는 분산 트랜잭셔널 키밸류 스토어로, 직렬화 가능 트랜잭션과 수평 확장을 제공한다.

결과: SQL 인터페이스는 SQLite 그대로, 내부 스토리지는 분산 트랜잭셔널 KV. 단일 DB 크기 한계가 사라지고, 멀티 노드가 같은 DB를 동시에 읽고 쓴다. 트레이드오프는 운영 — FoundationDB 자체가 운영이 만만하지 않다.

mvSQLite는 프로덕션 채택이 활발하다고 보기는 어렵지만, "SQLite의 API를 유지하면서 스토리지만 분산화한다" 는 디자인 어휘는 강력하다.

rqlite — Raft 위의 SQLite

rqlite는 SQLite 인스턴스 여러 개 앞에 Raft 합의 알고리즘 을 두고 강한 일관성을 만든 시스템이다. 모든 쓰기는 Raft 리더로 가고, 로그가 복제된 뒤에 각 노드의 SQLite에 적용된다.

특징:

  • CP 시스템 (CAP에서 일관성 우선).
  • 클러스터 크기는 보통 3~7대.
  • 매니지드 서비스가 아닌 자가 운영 이 기본.
  • HTTP API로 노출.

rqlite는 큰 회사보다는 엣지 디바이스(IoT, 산업용 게이트웨이)·소규모 클러스터·임베디드 분산 시나리오 에서 빛난다. "복잡한 분산 DB를 운영하기엔 작지만, SQLite의 단일 노드는 위험한" 케이스의 정답이다.

비교가 중요하다 — 다 같지 않다

시스템1차 디자인 축쓰기 토폴로지일관성
libSQL/Turso임베디드 레플리카 + 매니지드단일 1차, 비동기 복제결과적 (read-your-writes 옵션)
LiteFSFUSE 투명 복제 (일몰)단일 1차, 비동기 복제결과적
Cloudflare D1엣지 매니지드 + 읽기 레플리카단일 1차, 자동 리드 레플리카결과적 (읽기), 직렬화 (쓰기)
mvSQLiteFDB 백킹 분산 SQL멀티 라이터, FDB 합의직렬화 가능 (FDB 보장)
rqliteRaft 위의 SQLite리더 쓰기선형화 (Raft 보장)
평범한 SQLite + Litestream단일 노드 + S3 백업단일 라이터(백업/복구만)

이 표가 핵심이다 — "SQLite 기반"이라는 한 단어 안에 매우 다른 디자인이 들어 있다. 무엇을 풀고 싶은지 가 선택을 결정한다.


6장 · Litestream — 모두의 기반이 된 지속 백업

분산화를 원하지 않는다면? 그래도 백업은 필요하다. Litestream이 그 자리를 차지했다.

무엇을 하는가

Litestream은 SQLite의 WAL(Write-Ahead Log)을 거의 실시간으로 S3(또는 호환 스토리지)에 스트리밍 한다. 사이드카 프로세스로 돈다. 앱은 자기가 백업되고 있는지 모른다.

# 한 줄 셋업
litestream replicate /var/lib/app/data.db s3://my-bucket/backups/data.db

# 복구
litestream restore -o /var/lib/app/data.db s3://my-bucket/backups/data.db

복구는 PITR(Point-in-Time Recovery)이다 — 임의의 과거 시점으로 되돌릴 수 있다. RPO(복구 시점 목표)는 보통 초 단위.

왜 중요한가

Litestream은 "단일 노드 SQLite는 위험하다"는 마지막 정당화 를 깼다. 디스크가 죽어도, 인스턴스가 사라져도, 몇 분 안에 S3에서 복원된다. 운영 모델은 충격적일 만큼 단순하다 — 앱 1개, SQLite 파일 1개, Litestream 사이드카 1개. 끝.

LiteFS 일몰 이후 Fly와 다른 PaaS들이 Litestream을 더 추천하는 흐름이 강해졌다. "복잡한 분산 SQLite보다, 단일 노드 + Litestream이 90% 케이스에 더 낫다"는 입장이다.

저자(Ben Johnson, Fly.io)는 이후 LiteFS도 만든 사람인데, 본인이 직접 "대부분의 앱에는 Litestream이면 충분하다"고 자주 말한다. 통찰이다.


7장 · Bun SQLite + 네이티브 런타임 SQLite — 또 다른 부활의 축

런타임 레벨에서도 변화가 있다.

Bun의 bun:sqlite

Bun은 처음부터 bun:sqlite내장 모듈로 제공한다. 동기 API, 매우 빠르고, 의존성 0개.

import { Database } from 'bun:sqlite'

const db = new Database('app.db', { create: true })

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    email TEXT UNIQUE,
    created_at INTEGER DEFAULT (strftime('%s', 'now'))
  )
`)

// Prepared statement — N번 호출에도 한 번만 컴파일
const insert = db.prepare('INSERT INTO users (email) VALUES (?)')
const tx = db.transaction((emails: string[]) => {
  for (const e of emails) insert.run(e)
})
tx(['a@x.com', 'b@x.com', 'c@x.com'])

// Query
const all = db.query('SELECT id, email FROM users').all()
console.log(all)

이게 왜 새로운가? Node.js 진영에서는 SQLite를 쓰려면 외부 패키지(better-sqlite3, node-sqlite3)가 필요했다. Node.js 22+ 이후로는 실험적 node:sqlite가 들어왔다. 런타임이 SQLite를 1급 시민으로 받아들이기 시작한 것이다.

의미

이 흐름이 의미하는 것 — "앱 안에 SQL DB가 있다"는 모델이 더 이상 이상하지 않다. 별도 의존성 없이, 한 줄 임포트로. 이는 위에서 본 임베디드 레플리카 모델과 자연스럽게 결합한다. 런타임 자체가 SQLite를 알고, 라이브러리는 그 위에서 동기화를 한다.

Deno도, 그리고 모든 곳에서

Deno도 SQLite 표준 모듈을 제공한다. WASM 빌드의 SQLite(sql.js, wa-sqlite)는 브라우저에서 동작한다. 모든 런타임이 SQLite를 안다. 이 보편성이 SQLite를 "교환 표준" 으로 만든다 — 같은 파일을 서버, 클라이언트, 엣지가 모두 읽고 쓸 수 있다.


8장 · 로컬 우선 앱 — SQLite가 가능하게 한 패러다임

여기까지가 "서버 쪽 SQLite의 부활" 이라면, 더 큰 파도는 클라이언트다. 로컬 우선(local-first) 앱들.

정의

Ink & Switch의 정의를 요약하면 로컬 우선 앱은 다음을 만족한다:

  1. 즉각적 (네트워크 라운드트립 없음).
  2. 오프라인에서 동작.
  3. 다중 기기 동기화.
  4. 협업 가능.
  5. 데이터 소유권이 사용자에게.
  6. 보안과 프라이버시가 기본.
  7. 장기 수명 (서비스가 죽어도 데이터가 산다).

이를 위해서는 데이터가 클라이언트 안에 영속적으로 있어야 한다. 그 안의 SQL 엔진? SQLite다.

두 축은 다르다 — "로컬 우선" 과 "CRDT"

흔한 오해 하나를 짚자. "로컬 우선 = CRDT" 가 아니다. 둘은 직교한다.

  • 로컬 우선 은 데이터 토폴로지에 관한 약속이다 — 데이터가 클라이언트에 있다, 네트워크 없이도 동작한다.
  • CRDT(Conflict-free Replicated Data Type) 는 여러 클라이언트가 같은 데이터를 동시에 수정할 때 충돌을 해결하는 알고리즘이다.

협업이 없거나 단일 사용자가 다중 기기를 쓰는 경우(예: 개인 노트), 단순한 last-write-wins나 벡터 클럭으로 충분하다. CRDT는 다중 사용자 동시 편집에서 필요해진다(Figma, Linear 등).

SQLite는 로컬 우선의 영속성 기반 이다. CRDT 엔진(Automerge, Yjs)이나 SQL-네이티브 동기 엔진(ElectricSQL, Replicache, PowerSync)이 그 위에서 동기화를 다룬다.

SQL-네이티브 동기 엔진들

  • ElectricSQL — Postgres와 SQLite 사이의 양방향 동기. SQLite 안에 변경분이 모이고, 백그라운드로 Postgres와 머지. 충돌은 Rich-CRDT(reg. tree of operations)로 해결.
  • Replicache (Rocicorp) — 자체 데이터 모델, 클라이언트는 IndexedDB(또는 SQLite-WASM)를 쓰고, 서버는 push/pull 엔드포인트만 구현하면 됨.
  • PowerSync — Postgres 기반, 클라이언트는 SQLite. ElectricSQL과 경쟁/유사.

이들의 공통점: 클라이언트에 SQLite, 서버에 Postgres, 그 사이의 동기 레이어. SQLite의 부활은 단순히 서버 사이드만이 아니라, 이런 클라이언트-서버 토폴로지의 한쪽 끝을 책임지는 위치에서도 일어나고 있다.


9장 · SQLite가 옳을 때, 그리고 틀릴 때

여기까지 보면 SQLite 만능론처럼 들릴 수 있다. 그래서는 안 된다. 정직하게 보자.

SQLite가 옳은 케이스

  1. 읽기 헤비, 트래픽이 한 노드에 들어가는 앱. 대부분의 웹사이트, 대부분의 SaaS 백오피스, 거의 모든 블로그·문서 사이트. "QPS가 분당 1만 미만이고 단일 인스턴스로 충분" 이라면 SQLite가 종종 더 빠르다(네트워크 라운드트립이 없으므로).
  2. 엣지 네이티브 앱. Cloudflare Workers + D1, Fly.io + Litestream 모델. 데이터를 코드 옆에 두는 게 자연스럽다.
  3. 임베디드/IoT. 산업용 게이트웨이, 디바이스 로컬 로깅. SQLite의 본업.
  4. 로컬 우선 클라이언트. 데스크톱·모바일·PWA 앱의 영속 저장소.
  5. per-tenant 격리. 테넌트당 DB 1개. SQLite는 너무 가벼워서 수십만 개도 가능. Notion·Linear·Turso의 일부 디자인 패턴.
  6. 분석 쿼리의 임시 작업 공간. DuckDB가 더 어울리지만, 단순 케이스는 SQLite로도 충분.
  7. 테스트 환경의 격리된 DB. Postgres 대신 인메모리 SQLite. 빠르고 깨끗하다.

SQLite가 틀린 케이스

  1. 글로벌 단일 1차 + 다중 지역 동시 쓰기. 한 DB에 여러 지역에서 동시에 마이크로초 단위로 쓰기가 필요하면, SQLite 기반 시스템은 거의 다 단일 1차다. 다중 지역 쓰기는 Spanner, CockroachDB, YugabyteDB, FoundationDB 같은 진짜 분산 시스템의 영역이다.
  2. 쓰기 처리량이 코어 1개를 초과. SQLite는 쓰기에 단일 라이터 락이 있다(WAL 모드여도 트랜잭션 단위로 직렬화). 초당 수만 쓰기를 지속해야 한다면 다른 엔진을 봐라.
  3. 트랜잭션이 매우 길게 잡혀야 하는 OLTP. 긴 트랜잭션은 다른 쓰기를 막는다. Postgres MVCC가 훨씬 우아하다.
  4. 거대한 단일 DB. D1은 한 자릿수 GB가 한계, libSQL/Turso도 수십~수백 GB는 가능하지만 TB급 단일 DB는 어색하다. mvSQLite가 답일 수도 있지만, 그 시점에는 분산 OLTP를 다시 검토하라.
  5. 복잡한 분석 쿼리 (OLAP). SQLite 옵티마이저는 OLTP 워크로드에 맞춰져 있다. 대량 분석은 DuckDB·ClickHouse·Snowflake가 자기 자리다.
  6. 다중 라이터 어플리케이션이 데이터 무결성을 강하게 요구. 동시 쓰기 격리 수준이 필요하면 Postgres가 정답이다.
  7. 풀텍스트 검색 헤비. SQLite의 FTS5는 훌륭하지만 Elasticsearch·Meilisearch·Typesense 수준은 아니다.

결정 트리

시작:
  단일 노드에 들어가는가? (예: < 1만 QPS, < 100GB)
    └─ 예  → 다중 지역에 분산이 필요한가?
              ├─ 아니오 → SQLite + Litestream (가장 단순함)
              └─ 예    → 읽기/쓰기 비율은?
                         ├─ 읽기 헤비 → libSQL/Turso (임베디드 레플리카)
                         └─ 균형     → D1 (Workers라면) / Postgres + 리드 레플리카

    └─ 아니오 → 워크로드는?
                ├─ OLTP 분산   → CockroachDB / Spanner / YugabyteDB
                ├─ OLAP/분석   → ClickHouse / Snowflake / BigQuery
                └─ 멀티테넌트  → 테넌트당 SQLite (per-tenant) 진지하게 고려

이 결정 트리에서 가장 자주 답이 되는 것은 첫 번째 분기 다 — 대부분의 앱은 단일 노드에 들어간다. 그 사실을 인정하고 나면 운영의 단순함이 폭발적으로 좋아진다.


10장 · 운영의 실제 — 무엇을 진짜 신경 써야 하나

SQLite 기반 시스템을 운영할 때 자주 마주치는 함정들.

WAL 모드는 기본값으로

PRAGMA journal_mode=WAL;. 동시 읽기-쓰기 처리량이 극적으로 좋아진다. 거의 모든 라이브러리가 기본값으로 켜지만, 직접 만든 셋업이면 확인하라.

PRAGMA synchronous 의 트레이드오프

  • FULL (기본): 가장 안전, 가장 느림.
  • NORMAL: WAL 모드에서 권장. 매우 안전하고 훨씬 빠름.
  • OFF: 위험. 권장하지 않음.

백업은 무조건 Litestream 또는 동급

단일 노드 SQLite의 최대 위험은 디스크/인스턴스 손실 이다. Litestream으로 분 단위 RPO를 확보하라. EBS 스냅샷도 보완책으로 좋다.

분리 마이그레이션 + 앱 배포

SQLite 마이그레이션은 보통 빠르지만, 큰 테이블의 ALTER TABLE은 락이 걸린다. 큰 변경은 새 테이블 + 백필 + 스왑 패턴을 써라.

임베디드 레플리카의 동기 주기

Turso 임베디드 레플리카에서 syncInterval을 너무 짧게 잡으면(예: 1초) 네트워크와 CPU를 낭비한다. 보통 30~60초가 좋은 출발점이고, 쓰기 직후 즉시성이 필요한 곳에서는 명시적 db.sync()나 read-your-writes 모드를 써라.

per-tenant DB의 메타데이터 관리

테넌트당 DB 1개 패턴을 쓰면, "어느 테넌트 DB가 어느 인스턴스/리전에 있는지" 라우팅 메타데이터가 필요하다. 이 메타데이터 자체는 한 개의 중앙 DB(보통 Postgres 또는 더 큰 SQLite)에 둔다.


에필로그 — 부활은 우연이 아니다

24년 된 라이브러리가 가장 핫한 인프라가 된 것은 우연이 아니다. 세 가지가 동시에 일어났다 — 엣지 컴퓨트의 보편화, 로컬 우선 운동의 성숙, 그리고 "분산이 답이 아닐 수도 있다" 는 자각.

이 글의 한 줄 요약: SQLite는 새 도구가 아니라, 새 토폴로지를 가능하게 한 오래된 도구다. libSQL/Turso의 임베디드 레플리카, Cloudflare D1의 엣지 통합, Litestream의 백업 기반, Bun과 Node의 네이티브 SQLite — 이 모든 것이 같은 방향을 가리킨다. 데이터를 코드 옆에 두라.

체크리스트 — SQLite 기반 시스템 시작하기 전

  • 단일 노드에 들어가는가? (QPS, 데이터 크기 추산)
  • 읽기/쓰기 비율을 측정했는가? (90/10 이상이면 매우 유리)
  • 백업 전략이 있는가? (Litestream 또는 동급)
  • WAL 모드, synchronous=NORMAL 켰는가?
  • 마이그레이션 패턴을 정했는가?
  • 멀티 지역이 필요하다면, libSQL/Turso 또는 D1을 골랐는가?
  • 운영 단순성의 이점이 다른 DB의 분산 기능 손실을 상쇄하는가?

안티 패턴

  • "SQLite는 장난감 DB" 라는 편견으로 시작도 안 함. Notion, Linear, Cloudflare 등이 프로덕션에서 쓴다.
  • 거꾸로, SQLite로 다 풀려는 만능론. 9장의 "틀린 케이스" 를 인정하라.
  • 백업 없는 단일 노드 SQLite. 디스크는 죽는다.
  • 임베디드 레플리카에 강한 일관성을 기대. 모델 자체가 결과적 일관성이다.
  • LiteFS를 새 프로젝트에 도입. 일몰 흐름이다. libSQL이나 D1을 봐라.
  • 너무 잦은 db.sync() 호출. 60초 + 명시적 호출 조합이 보통 좋다.
  • 모든 분산 SQLite 시스템이 같은 보장을 한다고 가정. 표를 다시 보라 — rqlite는 선형화, Turso는 결과적, D1은 쓰기 직렬화 + 결과적 읽기다.

다음 글 예고

다음 두 편에서는 SQLite 르네상스의 자매 주제 두 가지를 다룬다:

  1. 로컬 우선 앱 구축 실습 — ElectricSQL과 PowerSync로 Postgres ↔ SQLite 양방향 동기 구현하기. 충돌 해결, 오프라인 큐, 모바일 클라이언트.
  2. 2026년의 데이터베이스 지도 — SQLite 르네상스, Postgres 18의 새 기능, DuckDB의 분석 침공, NewSQL의 현실, 그리고 벡터 DB가 결국 Postgres로 흡수되는 흐름.

24살 된 단일 파일 라이브러리가 가장 흥미로운 곳에 있다는 사실은 우리 업계의 진리 하나를 다시 가르친다 — 단순함은 다시 트렌드가 된다. 충분히 오래 기다리면.


참고 / References