왜 AI 에이전트 때문에 SQLite를 찾게 되는가
요약
AI 에이전트의 상태 저장(state management)을 위해 SQLite가 왜 매력적인 대안으로 떠오르는지 분석합니다. Turso와 같은 관리형 서비스의 등장으로 SQLite의 운영 제약이 해소되면서, 로컬 워크벤치와 중앙 원장의 조합이 새로운 아키텍처로 제시됩니다.
핵심 포인트
- AI 에이전트의 상태는 변화가 잦고 로컬/프라이빗한 특성을 가짐
- SQLite는 임베디드 방식으로서 에이전트의 상태 저장에 자연스러운 환경 제공
- Turso와 같은 생태계의 발전이 SQLite의 운영 제약(복제, 관리)을 해결
- 중앙 집중식 Postgres와 로컬 SQLite의 하이브리드 조합이 유망함
최근 저는 예전 같으면 생각 없이 Postgres를 선택했을 상황에서도 계속해서 SQLite를 찾게 됩니다.
작은 서비스들로 시작해서, 더 큰 질문으로 이어졌습니다. 멀티 테넌트 (multi-tenant) SaaS가 실제로 SQLite 위에서 구동될 수 있을까? 그리고 특히 AI 에이전트 (AI agents)의 경우, 로컬에 임베디드 (embedded)된 데이터베이스가 그들의 상태 (state)를 저장하기에 더 자연스러운 환경이 아닐까?
Turso는 제가 지금까지 발견한 이 스택의 가장 매력적인 버전이며, 특히 Cloudflare와 결합했을 때 더욱 그렇습니다. D1이 Turso 수준의 임베디드 복제본 (embedded-replica) 성능을 갖추기를 바라며, AWS가 Postgres를 위한 RDS를 제공하는 것처럼 관리형 SQLite 스타일의 서비스를 제공하기를 바랍니다.
이것은 "Postgres의 시대가 끝났다"는 주장이 아닙니다. 저는 여전히 SQLite보다 Postgres를 더 자주 사용합니다. 또한 이것은 조언도 아닙니다. 단지 최근 저의 생각이 흘러가고 있는 방향일 뿐이며, 제가 어디서 틀렸는지 찾아내기 위해 기록해 둔 것입니다. 권장 사항이 아닌 한 개인의 노트로 읽어주세요.
현재 제가 내린 결론 (그리고 계속 수정해 나갈 것으로 예상하는 부분):
- SQLite가 Postgres를 대체하는 것은 아닙니다.
- **작업 상태 (work state)**를 위해서는, SQLite가 점점 더 제 마지막 선택지가 아닌 첫 번째 선택지가 되고 있습니다.
- AI 에이전트 (AI agents)가 이를 더욱 가속화합니다: 그들의 상태는 변화가 잦고 (high-churn), 로컬이며, 대부분 프라이빗 (private)합니다.
- 정답은 전부 로컬인 것이 아닙니다. 로컬 **워크벤치 (workbench)**와 중앙 **원장 (ledger)**의 조합입니다.
과거의 기본 설정이 존재했던 이유
수년 동안 "데이터가 어디에 있는가?"라는 질문에는 한 가지 실질적인 답이 있었습니다. 바로 API 뒤에 있는 서버, 공유된 Postgres 안에 있는 것이었습니다. 그중 상당수는 아키텍처 때문이 아니라, 사용 가능한 가장 저렴한 형태였기 때문입니다.
SQLite는 이미 어디에나 있었지만, 데이터베이스를 SaaS 인프라로서 실행 가능하게 만드는 운영 계층(operational layer)이 부족했습니다. 즉, 네트워킹, 복제 (replication), 관리형 백업, 그리고 수많은 작은 데이터베이스를 도구의 홍수에 빠지지 않고 실행할 수 있는 방법이 없었습니다. 그래서 중앙 집중화가 저항이 가장 적은 경로였고, 공유된 Postgres에 tenant_id 컬럼을 두는 것이 반사적인 선택이 되었습니다.
변화한 것은 SQLite가 아닙니다. 생태계가 부족했던 부분들을 채워 넣었다는 점입니다. 그리고 증가하는 워크로드 (workloads) 계층에게, 가장 빈번하게 쓰기 작업을 수행하는 요소가 제 자신의 머신으로 이동했습니다.
해소되고 있는 제약 사항
SQLite 자체는 설계상 다음과 같습니다:
- 네트워크 방식이 아닌 임베디드 (Embedded, not networked) — 라이브러리 형태이며, 포트에서 대기(listen)하는 프로세스가 없습니다.
- 복제되지 않는 단일 파일 (Single-file, not replicated) — 내장된 읽기 복제본(read replicas)이나 장애 조치(failover) 기능이 없습니다.
- 단일 쓰기 (Single-writer) — WAL(Write-Ahead Logging)은 다수의 읽기 작업과 _단 하나_의 쓰기 작업을 허용합니다. 경합이 발생하면
SQLITE_BUSY오류를 마주하게 됩니다.
이러한 문제들은 SQLite 자체를 변경함으로써 해결된 것이 아닙니다. SQLite의 _주변(around)_에서 해결되었습니다:
- libSQL (Turso의 SQLite 호환 포크) — 관리형 서버 레이어, 원격 액세스, 임베디드 복제본, 암호화, 벡터 검색(vector search)을 제공합니다. (처음부터 다시 만든 Rust 기반의 후속작인 "Limbo"는 현재 Turso Database로 불리며, 별도의 덜 성숙한 트랙이므로 이 둘을 혼동하지 마세요.)
- 임베디드 복제본 (Embedded replicas) — 프로세스 내부로 동기화된 완전한 로컬 SQLite 복사본입니다. 읽기는 로컬에서 수행하고, 쓰기는 기본(primary) 서버로 전달됩니다.
- Cloudflare D1 — 글로벌 읽기 복제(read replication)와 세션 API(Sessions API)를 통한 읽기-자신의-쓰기(read-your-writes)를 지원하는 관리형 서버리스 SQLite입니다.
- Durable Objects — 코드와 동일한 스레드 내에서 실행되는 SQLite 데이터베이스입니다.
- Litestream / LiteFS — 쓰기 경로를 건드리지 않고, 내구성을 위해 WAL을 객체 스토리지(object storage)로 스트리밍합니다.
단일 쓰기 모델은 사라지지 않았으며, 저 또한 여전히 이를 고려하여 설계합니다. 하지만 "SQLite는 네트워크화하거나 복제할 수 없으며, 공유 인프라로 실행할 수 없다"는 말은 실무적으로 더 이상 완전히 사실이 아닙니다. 따라서 과거에 반사적으로 중앙 집중화를 선택했던 이유 중 일부는 이제 예전만큼 유효하지 않습니다.
상태 중력 (State gravity)
트레이드오프(trade-off)를 명확히 하기 위해 제가 사용하는 느슨한 모델은 다음과 같습니다: 상태(state)는 그것을 가장 많이 읽고 쓰는 쪽으로 끌려간다. 쓰기 주체(writer)를 상태로부터 멀리 두면 모든 작업에서 거리 비용(지연 시간, 데이터 송출(egress), 풀(pool), 일관성 조정 등)을 지불해야 합니다. 반면 이들을 가깝게 두면 그러한 비용의 대부분이 사라집니다.
지난 20년 동안 쓰기 주체는 웹 앱을 클릭하는 인간이었습니다. 작업 규모가 작고 간헐적이었으며, 왕복 시간(round-trip)에 관대했습니다. 끌어당기는 힘(pull)이 약했기에 중앙 집중식 상태(central state) 방식이 적절했습니다.
그 작성자(writer)는 변했습니다. 유용한 에이전트들은 작업 환경(work environment) — 파일 시스템(file system), 터미널(terminal), IDE, 브라우저 탭, 이미 인증된 세션(authenticated session) — 의
_내부(inside)_에서 실행됩니다. Codex CLI는 사용자가 선택한 디렉토리를 대상으로 로컬 터미널에서 실행되며, Claude Code는 코드베이스(codebase)를 읽고, 파일을 수정하며, 사용자의 머신에서 명령을 실행합니다. 그 강점은 모델의 품질만큼이나
근접성 (proximity)
에 있습니다.
에이전트가 작동하는 동안, 에이전트는 많은 상태(state)를 보유합니다: 작업 계획(task plan), 파일 인덱스(file index), 스테이징/언스테이징된 차이점(staged/unstaged diffs), 도구 호출 이력(tool-call history), 실패 및 재시도(failures and retries), 승인 상태(approval state), 로컬 검색 인덱스(local search index), 임베딩 캐시(embeddings cache), 동기화 커서(sync cursors), 멱등성 키(idempotency keys), 실행 취소/다시 실행 로그(undo/redo log).
이는 변화가 잦고(high-churn), 로컬(local)하며, 다른 누구에게도 쓸모가 없습니다. 이 모든 것을 중앙 Postgres로 왕복(round-tripping)시키는 것은 세 가지 측면에서 비용이 많이 듭니다:
- 지연 시간 (Latency) — 로컬 SQLite 읽기는 네트워크를 통한 읽기보다 한 자릿수에서 여러 자릿수만큼 더 빠른 경향이 있습니다(워크로드에 따라 다름). 임베디드 복제본(embedded replica)은 로컬 파일에서 즉시 응답하지만, 원격 읽기는 먼저 네트워크 비용을 지불해야 합니다.
- 비용 (Cost) — 한 시간 뒤에 삭제될 임시 작업(scratch work)을 저장하기 위해 원장급(ledger-grade) 인프라를 사용하는 격입니다.
- 개인정보 보호 (Privacy) — 에이전트가 가진
당신의
파일 인덱스가 머신을 떠나는 순간, 당신은 불필요한 데이터 처리 문제(data-handling problem)를 야기하게 됩니다.
작성자(writer)가 이동하면 → 작업 상태(work state)도 따라갑니다.
로컬은 단순히 더 가까운 것이 아니라 — 더 큽니다
근접성은 한 가지 이유일 뿐입니다. 제가 가장 과소평가되었다고 생각하는 이유는 순수 하드웨어(raw hardware)입니다. 하지만 이 부분이 과하게 확장되기 쉽기 때문에,
어떤
경우에 적용되는지에 대해서는 신중하고 싶습니다.
이것은 사용자의 로컬 머신에서 실행되는 상주 에이전트 (resident agent) — 데스크톱/CLI 앱으로 실행되는 Claude Code, Codex 등과 같이 명확하게 데스크톱 형태의 워크로드(workload)를 가진 경우에 적용됩니다. 각 측면이 단일 사용자에게 제공하는 것을 비교해 보십시오. 현대적인 노트북만 놓고 보더라도, 독점적으로 수 개에서 십수 개 이상의 CPU 코어, 수십 GB의 RAM, 빠른 NVMe SSD, 로컬 파일 시스템, 때로는 GPU/NPU, 그리고 이미 사용자로 인증된 세션을 제공합니다. 반면 클라우드 멀티 테넌트 (multi-tenant) 런타임은 동일한 사용자에게 제한된 CPU, 메모리, 디스크 I/O, 영구 저장소와 같은 '조각 (slice)'만을 제공하며, 서버 측에서 도구를 안전하게 실행하려면 샌드박스 (sandboxes), VM, 네트워크 제어, 권한 부여, 감사 로깅 (audit logging), 큐잉 (queueing) 등이 추가로 필요합니다. 따라서 네트워크 지연 시간 (network latency) 문제가 등장하기 전이라도, 사용자당 리소스 측면에서는 로컬 머신이 승리하는 경우가 많으며, 네트워크는 그 격차를 더 벌릴 뿐입니다. 모델은 클라우드에서 '생각 (think)'하게 두십시오. 읽기, 쓰기, 차이점 비교 (diffing), 인덱싱 (indexing), 검색 (searching), 캐싱 (caching)은 하드웨어가 실제로 존재하는 곳에서 수행하는 것이 더 저렴하며, 이것이 바로 로컬 상태 (local state)를 생성하는 작업입니다.
이것이 서버 측 멀티 테넌트 SaaS 에이전트 — 즉, 에이전트가 사용자의 노트북이 아닌 귀하의 자체 인프라에서 실행되는 경우 — 에 자동으로 적용되는 것은 아닙니다. 루프 안에
- 10,000번째 테넌트(Tenant)도 그저 또 하나의 파일일 뿐입니다 — 데이터베이스당 별도의 프로세스도, 콜드 스타트(Cold start)도 없습니다.
- 비용은 데이터베이스의 _개수_가 아니라 _사용량_에 따라 확장됩니다. Turso의 모델에서 데이터베이스 개수는 Postgres에서처럼 희소한 단위가 아닙니다. D1의 현재 제한 사항은 계정당 최대 50,000개의 데이터베이스를 허용하며, 과금은 데이터베이스당 서버 기준이 아닌 행(row) 수 + 스토리지(storage)를 기반으로 합니다.
- 격리(Isolation)는
WHERE절에 의존하여 절대 잊지 않기를 기도해야 하는 방식이 아니라, **물리적(physical)**입니다. - 백업, PITR(Point-in-Time Recovery), 그리고 내보내기(export)가 파일 단위 작업이 됩니다. "이 고객을 삭제하라"는 명령은 파일 단위 작업에 가까워집니다 — 백업, 로그, 데이터 웨어하우스(warehouse), 검색 인덱스(search indexes), 감사 추적(audit trails)이 여전히 중요하므로 문자 그대로의
rm은 아니지만, 운영 상태가 공유 테이블에 흩어져 있지 않게 됩니다. - 폭발 반경(Blast radius)이 제한됩니다 — 잘못된 마이그레이션(migration)이 발생해도 공유 클러스터가 아닌 하나의 파일에만 영향을 미칩니다.
정직한 비용(The honest costs):
- 테넌트 간 쿼리(Cross-tenant queries)가 더 어려워집니다. 런타임에 10,000개의 파일을 쿼리하는 것으로 해결되는 것이 아니라, 테넌트당
outbox→ 이벤트 스트림(event stream) → 중앙 데이터 웨어하우스(central warehouse) 구조로 해결해야 합니다. 이는 이제 귀하가 소유해야 하는 인프라입니다. - 마이그레이션이 확산(fan out)됩니다. 부분적 실패 상태를 포함한 N개의 마이그레이션이 발생할 수 있습니다. 따라서 "모든 테넌트 DB에 적용"을 일급 객체(first-class)이자 재개 가능한(resumable) 작업으로 취급하는 툴링(tooling)이 필요합니다.
- 단일 쓰기 제한(Single-writer ceiling)은 데이터베이스당 적용됩니다. 보통은 괜찮습니다. 쓰기 성능을 완전히 포화시키는 테넌트가 있다면 다른 환경이 필요하며, 격리(isolation)를 통해 오직 그 테넌트만이 영향을 받게 됩니다.
- D1 형태의 제한 사항 — 데이터베이스당 10GB, 데이터베이스당 단일 스레드 쿼리 실행.
트레이드오프(The trade): 손쉬운 테넌트 간 분석(cross-tenant analytics)과 단일 마이그레이션의 단순함을 포기하는 대신, 물리적 격리, 더 완만한 비용 구조, 단순한 테넌트별 라이프사이클 운영, 제한된 폭발 반경을 얻습니다. 제가 작업하는 많은 B2B SaaS의 경우, 최근에는 이것이 좋은 트레이드오프라고 느껴졌습니다. 다만, 최종적인 결과는 워크로드(workload)에 따라 크게 달라지므로 그 이상으로 일반화하여 말하지는 않겠습니다.
두 가지 지연 시간 계층 (Two latency tiers)
PRIMARY (source of truth)
│ WAL frames
▼
...
- 지역별 읽기 복제본(Regional read replicas) (D1, Turso cloud)은 데이터를 사용자 근처에 배치합니다. D1의 읽기 지연 시간(read latency)은 Worker 근처에서는 매우 좋을 수 있지만, 여전히 _네트워크_를 통한 읽기입니다.
- 임베디드 복제본(Embedded replicas) (Turso/libSQL)은 실제 SQLite 파일을 _프로세스 내부로 동기화_하여, 읽기가 절대 외부로 나가지 않게 합니다. 이 방식이 에이전트에게 필요한 수준인데, 에이전트는 자신의 상태를 끊임없이 읽기 때문입니다.
둘 다 쓰기는 단일 기본(single primary)으로 라우팅됩니다. 즉, **쓰기 지연 시간 하한선(write-latency floor)**이라는 규칙이 다시 나타납니다. US-West 기본 + 싱가포르 작성자 = 무조건 태평양 왕복 시간이 걸립니다. 따라서 읽기 지역성(read-locality)을 위해 설계하고 쓰기의 중앙 집중화(write-centralization)를 받아들이는 것이 편리하며, 이는
- "SQLite는 여전히 단일 쓰기(one writer)만 지원한다." 맞습니다. 저는 높은 쓰기 동시성(write concurrency)을 주장하는 것이 아닙니다. 적절한 경계(boundary)를 설정하고 나면, 많은 워크로드(workload)가 하나의 데이터베이스 내부에서 동시성을 필요로 하지 않는다는 점을 주장하는 것입니다. 이는 모든 것을 중앙 집중화하라는 이유가 아니라, 경계를 잘 설정해야 하는 이유입니다.
- "Postgres도 멀티 테넌시(multi-tenant)를 저렴하게 처리한다." 맞습니다. Neon, Aurora Serverless, Supabase, RLS 등이 있죠. 핵심은 "Postgres가 나쁘다"는 것이 아닙니다. 저에게 있어 "공유 Postgres +
tenant_id" 방식은 더 이상 자동적인 시작점이 아니라, 다른 옵션들과 함께 고려해야 할 하나의 선택지가 되었다는 뜻입니다. - "로컬 퍼스트 동기화(Local-first sync)는 어렵다." 가장 핵심적인 난관입니다. 동기화(Sync)는 백그라운드 작업이 아니라 제품의 표면(product surface)입니다. 두 액터(actor)가 동일한 객체를 편집한다면, 데이터베이스에 대한 의견을 정하기 전에 충돌 모델(conflict model)이 필요합니다. 테넌트(tenant)들이 쓰기 가능한 상태(writable state)를 공유하지 않는다면, 테넌트별(per-tenant) 방식이 가장 쉽습니다.
- "테넌트 간 분석(Cross-tenant analytics)이 고통스러워진다." 그렇습니다. 무료로 누리던
JOIN을 잃게 되고, 이를 데이터 웨어하우스(warehouse)를 통해 다시 구매해야 합니다. 모든 테넌트에 걸친 분석 우선(analytics-first) 방식인가요? 그렇다면 공유 Postgres가 여전히 승리할 수 있습니다. 롤업(roll-ups)을 포함한 테넌트 격리 방식인가요? 그렇다면 테넌트별 방식이 승리하는 경향이 있습니다.
모든 것이 SQLite인 것은 아닌 지점
엄격한 중앙 저장소(central store)를 유지하게 만드는 세 가지 요소가 있습니다:
- 협업(Collaboration) — 공유되고 동시에 편집되는 객체에는 동기화(sync), 충돌 해결(conflict resolution), 권한(permissions), 그리고 수렴된 뷰(converged view)가 필요합니다. 로컬 SQLite는 이 중 어느 것도 제공하지 않습니다.
- 책임 추적성(Accountability) — 누가 명령했는지, 어떤 모델을 사용했는지, 어떤 권한이 있었는지, 무엇이 변경되었는지에 대한 정보입니다. 그러한 추적(trace) 데이터는 사건이 발생한 노트북에만 머물러 있을 수 없습니다.
- 전역적 진실(Global truth) — 돈, 재고, 결제, 계약 등입니다. 이것들이 엣지(edge)에 흩어져 있으면, 시스템은 존재하는 금액에 대해 스스로 모순된 정보를 가질 수 있습니다. 원장(ledger)은 중앙에, 엄격하게, 단일하게 유지되어야 합니다.
따라서 그 형태는 하나의 경사면(gradient)과 같습니다. 엣지에서는 빠르고 일회적(disposable)이며, 중앙에서는 느리고 엄격합니다:
작업 시스템(SYSTEM OF WORK) — 작업대 (로컬 · 높은 변동성 · 일회적)
┌──────────────────────────────────────────────┐
│ 로컬 SQLite 에이전트 / 디바이스 작업 │
...
Postgres가 사라지는 것은 아닙니다. 다만 제가 작업을 시작하는 지점에서 벗어날 뿐이며, Postgres의 무료 교차 테넌트 분석 (cross-tenant analytics) 기능은 데이터 웨어하우스 (warehouse)로 이동합니다.
테스트: 하나의 변경 사항 추적하기
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기