← 목록으로 돌아가기

SQLite를 프로덕션에서 쓴다는 것: libSQL·Turso·WAL 모드로 서버리스 아키텍처 재설계하기

Database

SQLite embedded production libSQL Turso WAL mode

SQLite가 프로덕션용으로 적합한 워크로드

SQLite를 프로덕션에서 쓴다는 말을 꺼내면 종종 이런 반응이 돌아옵니다. "그거 테스트용 아닌가요?" 2026년 현재, 이 인식은 현실과 상당히 멀어져 있습니다. SQLite는 전 세계에서 가장 많이 배포된 데이터베이스 엔진이고, Android 기기, iOS 앱, 브라우저의 Web Storage, 항공기 시스템까지 광범위하게 쓰입니다.

SQLite가 프로덕션에서 강점을 발휘하는 워크로드는 분명합니다. 읽기 중심의 콘텐츠 서비스, 한 테넌트당 데이터베이스 파일 하나를 할당하는 멀티테넌시 SaaS, 엣지 환경에서 로컬 캐시 레이어가 필요한 서버리스 함수, 그리고 소규모 팀이 운영하는 관리 도구나 내부 시스템이 대표적입니다. 반면 초당 수백 건의 쓰기 트랜잭션이 동시에 몰리는 고동시성 워크로드, 지리적으로 분산된 데이터베이스 레플리케이션, 무거운 집계 연산이 실시간으로 필요한 OLAP 쿼리에는 맞지 않습니다.

우리 팀이 콘텐츠 퍼블리싱 플랫폼에서 Cloudflare Workers 기반 API를 운영할 때 PostgreSQL RDS 연결 비용이 생각보다 큰 문제였습니다. 콜드 스타트마다 커넥션 풀을 초기화하는 시간이 p99 지연 시간을 크게 늘렸습니다. 서버리스 환경에서 "연결을 여는 비용"이 없는 임베디드 데이터베이스는 단순한 성능 최적화가 아니라 아키텍처적 선택입니다.


1. WAL 모드와 동시성 한계

SQLite의 기본 저널링 모드는 DELETE 모드입니다. 쓰기 트랜잭션이 시작되면 데이터베이스 파일 전체에 독점 락(exclusive lock)을 획득하고, 변경이 완료될 때까지 다른 모든 읽기와 쓰기를 차단합니다.

WAL(Write-Ahead Logging) 모드는 이 동시성 문제를 크게 개선합니다. 공식 SQLite WAL 문서에 따르면, WAL 모드에서는 변경 사항이 원래 데이터베이스 파일에 직접 쓰이지 않고 별도의 WAL 파일에 먼저 기록됩니다. 이 구조 덕분에 읽기와 쓰기가 서로를 차단하지 않습니다.

-- WAL 모드 활성화
PRAGMA journal_mode = WAL;

-- 동기화 설정 (NORMAL은 대부분의 워크로드에 적합)
PRAGMA synchronous = NORMAL;

-- WAL 파일이 너무 커지지 않도록 자동 체크포인트 설정
PRAGMA wal_autocheckpoint = 1000;

-- 현재 저널 모드 확인
PRAGMA journal_mode;

WAL 모드에서도 해소되지 않는 한계가 있습니다. 쓰기는 여전히 직렬화됩니다. 동시에 여러 쓰기 트랜잭션이 들어오면 순서대로 처리되므로, 초당 수백 건의 동시 쓰기가 필요한 워크로드에서는 단순 WAL 모드만으로 해결이 안 됩니다.

모드읽기-쓰기 동시성동시 쓰기파일 구조권장 시나리오
DELETE (기본)불가직렬화파일 1개단순 CLI 도구, 테스트
WAL가능직렬화파일 3개프로덕션 읽기 중심 서비스
MEMORY인메모리테스트 전용

2. libSQL이란: SQLite의 fork와 차이점

libSQL은 SQLite의 오픈소스 fork입니다. SQLite의 라이선스는 퍼블릭 도메인이지만, 공식 SQLite 프로젝트는 외부 기여를 받지 않는 특이한 개발 구조를 유지합니다. libSQL은 이 제약 없이 SQLite 기반에서 새로운 기능을 추가하기 위해 시작된 프로젝트입니다.

github.com/tursodatabase/libsql 리포지터리를 보면 libSQL이 SQLite와 호환되는 API를 유지하면서 추가하는 핵심 기능들이 보입니다.

  • 벡터 유사도 검색: AI 임베딩을 저장하고 코사인 유사도 검색을 SQL 안에서 수행할 수 있는 확장이 내장.
  • 원격 연결 지원: hrana 프로토콜을 통해 서버에서 실행 중인 libSQL 인스턴스에 TCP 또는 WebSocket으로 연결.
  • Encryption at Rest: 데이터베이스 파일 자체를 암호화하는 기능이 내장.
  • 완전한 SQLite 호환성: 기존 SQLite 드라이버를 쓰던 코드에서 대부분 바꿀 것이 없음.

주의할 점은 libSQL이 SQLite의 완벽한 drop-in 대체재를 표방하지만, 일부 내부 구현 차이로 인해 엣지 케이스에서 동작이 다를 수 있다는 것입니다.


3. Turso 엣지 DB 아키텍처

Turso는 libSQL을 기반으로 만들어진 엣지 데이터베이스 서비스입니다. 단순히 libSQL을 호스팅하는 것을 넘어서, 지리적으로 분산된 읽기 레플리카를 통해 사용자와 가까운 엣지에서 SQLite 수준의 빠른 읽기를 가능하게 합니다.

Turso의 아키텍처는 세 레이어로 구성됩니다. 첫째, 단일 Primary 인스턴스가 모든 쓰기를 처리합니다. 둘째, 전 세계 여러 리전에 배치된 Replica 인스턴스가 Primary로부터 비동기적으로 변경 사항을 받아 읽기 요청에 응답합니다. 셋째, 클라이언트 애플리케이션은 가장 가까운 Replica에 자동으로 연결됩니다.

이 구조의 핵심 트레이드오프는 **최종 일관성(Eventual Consistency)**입니다. 쓰기가 Primary에 반영된 뒤 Replica에 전파되기까지 수십 밀리초의 지연이 있을 수 있습니다.

Turso는 데이터베이스 단위 격리를 지원합니다. 멀티테넌시 SaaS에서 테넌트당 데이터베이스를 생성하는 패턴을 API로 자동화할 수 있고, 수천 개의 데이터베이스를 하나의 계정에서 관리할 수 있습니다.


4. Embedded Replica 패턴

Embedded Replica는 libSQL과 Turso가 제공하는 가장 실용적인 아키텍처 패턴입니다. 애플리케이션 서버 로컬에 SQLite 파일을 두고, 이 파일을 원격 Turso 데이터베이스와 동기화합니다. 읽기는 로컬 파일에서 처리하므로 네트워크 레이턴시가 없고, 쓰기는 원격 Primary로 전달됩니다.

Embedded Replica는 특히 Next.js, Remix 같은 Node.js 기반 프레임워크로 운영하는 단일 서버 환경에서 효과적입니다. 단, 서버 인스턴스가 여러 개로 스케일 아웃되는 환경에서는 각 인스턴스가 독립적인 로컬 파일을 가지게 되므로, 인스턴스 간 데이터 동기화가 원격 데이터베이스를 통해 이루어지는 구조를 이해하고 설계해야 합니다.


5. ORM 도구(Drizzle, Prisma) 호환성

Drizzle ORM은 libSQL과의 궁합이 현재 시점에서 가장 좋습니다. @libsql/client 패키지를 직접 드라이버로 받아 Drizzle에 연결하면 됩니다.

import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import * as schema from './schema';

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN,
  ...(process.env.TURSO_SYNC_URL
    ? {
        syncUrl: process.env.TURSO_SYNC_URL,
        syncInterval: 60,
      }
    : {}),
});

export const db = drizzle(client, { schema });

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const posts = sqliteTable('posts', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content').notNull(),
  publishedAt: integer('published_at', { mode: 'timestamp' }),
  authorId: text('author_id').notNull(),
});

Prisma는 SQLite를 오랫동안 지원해왔고, libSQL을 위한 드라이버 어댑터도 제공합니다. 다만 Prisma의 libSQL 지원은 Drizzle보다 설정이 복잡합니다.

무중단 스키마 변경이 필요한 경우에는 Expand and Contract 패턴을 SQLite 환경에 맞게 적용해야 합니다.


6. Next.js Edge Runtime과 결합

Next.js의 Edge Runtime은 V8 기반의 경량 런타임으로, Node.js의 모든 API를 지원하지 않습니다. 파일 시스템 접근이 없으므로 로컬 SQLite 파일을 직접 읽을 수 없습니다.

import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@libsql/client/http';
import { drizzle } from 'drizzle-orm/libsql';
import { posts } from '@/lib/schema';
import { desc, eq } from 'drizzle-orm';

export const runtime = 'edge';

const client = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

const db = drizzle(client);

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const authorId = searchParams.get('authorId');

  const result = await db
    .select({
      id: posts.id,
      title: posts.title,
      publishedAt: posts.publishedAt,
    })
    .from(posts)
    .where(authorId ? eq(posts.authorId, authorId) : undefined)
    .orderBy(desc(posts.publishedAt))
    .limit(20);

  return NextResponse.json({ posts: result });
}

Edge Runtime과 libSQL을 결합할 때 모듈 수준에서 클라이언트를 초기화해 재사용해야 합니다.


7. 백업·복구 전략

SQLite 데이터베이스 파일은 단일 파일이므로 백업이 직관적으로 느껴집니다. 그러나 WAL 모드에서 동작 중인 데이터베이스 파일을 단순히 복사하면 불완전한 백업이 될 수 있습니다.

-- 온라인 백업: WAL 체크포인트 후 복사
PRAGMA wal_checkpoint(TRUNCATE);

-- VACUUM INTO: 최적화된 새 파일 생성 (SQLite 3.27.0+)
VACUUM INTO '/path/to/backup-2026-05-12.db';

Turso를 사용한다면 플랫폼 자체에서 자동 백업을 제공합니다. 자체 호스팅 libSQL 환경이라면 Litestream이 SQLite 데이터베이스의 변경 사항을 WAL 레벨에서 실시간으로 S3, GCS 같은 오브젝트 스토리지로 스트리밍합니다.

대규모 파티셔닝이 필요한 데이터 보관 전략은 PostgreSQL 파티셔닝 운영 가이드에서 다루는 접근 방식과 유사하지만, SQLite에서는 파티셔닝 대신 기간별 별도 데이터베이스 파일을 운영하고 ATTACH DATABASE로 조인하는 방식을 쓸 수 있습니다.


8. PostgreSQL 대비 비용·운영 비교

항목SQLite + TursoPostgreSQL
초기 설정 복잡도낮음중간~높음
읽기 레이턴시 (Embedded Replica)마이크로초 수준수 밀리초
동시 쓰기 처리량낮음 (직렬화)높음 (MVCC)
글로벌 엣지 배포Turso 기본 제공별도 레플리케이션 설계 필요
스케일 아웃 (읽기)Turso Replica 자동읽기 레플리카 수동 설정
비용 (소규모)무료 플랜 존재인스턴스 비용 상시 발생
운영 전문성 요구낮음중간~높음

비용 관점에서 SQLite + Turso는 트래픽이 적거나 읽기 중심인 서비스에서 PostgreSQL보다 유리한 경우가 있습니다.


9. 적합/부적합 케이스 결정 기준

적합한 케이스:

  • 읽기 대 쓰기 비율이 10:1 이상인 콘텐츠 서비스
  • 테넌트당 데이터베이스 격리가 필요한 멀티테넌시 SaaS
  • Cloudflare Workers, Vercel Edge Functions처럼 파일 시스템 없는 서버리스 환경
  • 소규모 팀이 운영하는 내부 도구, 대시보드
  • 초기 스타트업 단계에서 운영 복잡도를 최소화하고 싶은 경우

부적합한 케이스:

  • 초당 수백 건 이상의 동시 쓰기 트랜잭션이 필요한 결제, 재고 관리 시스템
  • 강한 일관성이 필수인 금융 트랜잭션 처리
  • 여러 테이블을 아우르는 복잡한 OLAP 집계 쿼리가 주요 워크로드
  • 지리적으로 분산된 여러 인스턴스가 동시에 쓰기를 수행

10. 결론

SQLite를 프로덕션에서 쓰는 것은 2026년 현재 무모한 시도가 아닙니다. WAL 모드, libSQL, Turso, Embedded Replica, Drizzle ORM의 조합은 적절한 워크로드에서 PostgreSQL보다 단순하고 빠른 아키텍처를 만들어낼 수 있습니다.

  • WAL 모드 적용 여부 확인: 프로덕션 SQLite 데이터베이스는 반드시 PRAGMA journal_mode = WAL을 적용한다.
  • 쓰기 동시성 요구사항 정량화.
  • Embedded Replica 환경 제약 확인.
  • 백업 및 복구 절차 사전 구성: Litestream이나 Turso 플랫폼 백업을 설정하고, 복원 시나리오를 실제로 수행해본다.
  • 마이그레이션 경로 설계: 서비스가 성장해 SQLite의 한계에 도달했을 때 PostgreSQL로 전환하는 경로를 미리 설계한다.