Skip to content

필사 모드: SQLite는 어디에나 있다

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 세상에서 가장 조용한 지배자

데이터베이스 이야기를 하면 사람들은 보통 PostgreSQL, MySQL, Oracle, 요즘이면 클라우드 데이터 웨어하우스를 떠올립니다. 그런데 지구상에서 가장 널리 배포된 데이터베이스 엔진은 그중 어느 것도 아닙니다. 정답은 SQLite입니다.

SQLite는 아마 세계에서 가장 많이 실행되고 있는 데이터베이스일 것입니다. 보수적으로 잡아도 활성 사용 중인 인스턴스가 조 단위(trillion)로 추정됩니다. 왜냐하면 어디에나 있으니까요. 여러분 주머니 속 안드로이드폰과 아이폰에 들어 있고, 크롬·사파리·파이어폭스 같은 모든 주요 브라우저 안에서 돌고, 데스크톱 앱의 설정과 캐시를 저장하고, 심지어 상용 여객기의 항공전자 시스템에도 쓰입니다. 그런데도 SQLite는 조용합니다. 서버 프로세스도, 관리자도, 포트 번호도 없습니다. 그냥 라이브러리로 앱 안에 링크되어 묵묵히 일합니다.

이 글은 이 조용한 지배자가 어떻게 설계되었길래 이렇게 널리 퍼졌는지, 언제 SQLite가 무거운 클라이언트-서버 DB를 실제로 이기는지, 그리고 프로덕션에서 SQLite를 제대로 쓰기 위한 실무 지식을 정리합니다.

서버가 없다 — 단일 파일이라는 급진적 설계

SQLite를 이해하는 첫 열쇠는 그것이 **서버가 없다(serverless)**는 점입니다. 요즘 유행하는 "서버리스 클라우드"와는 다른 의미입니다. 여기서 서버리스란, SQLite에는 별도로 떠 있는 데이터베이스 서버 프로세스가 아예 없다는 뜻입니다.

전형적인 데이터베이스를 떠올려 봅시다. PostgreSQL을 쓴다면 어딘가에 postgres 서버 프로세스가 돌고 있고, 애플리케이션은 네트워크 소켓(보통 TCP 5432 포트)을 통해 그 서버에 연결해 SQL을 주고받습니다. 서버는 연결을 관리하고, 인증을 처리하고, 동시 접근을 조율합니다. 이 구조는 강력하지만 무겁습니다. 서버를 설치하고, 실행하고, 설정하고, 보안을 챙기고, 죽지 않게 감시해야 합니다.

SQLite는 이 전체 계층을 없앱니다.

  전형적인 클라이언트-서버 DB
  +-------------+     네트워크     +------------------+     +----------+
  | 애플리케이션 | <--- 소켓 ---> | DB 서버 프로세스  | --> | 디스크    |
  +-------------+                 +------------------+     +----------+

  SQLite
  +-----------------------------------+     +----------+
  | 애플리케이션 (SQLite가 라이브러리로   | --> | 단일 파일 |
  |  링크되어 함수 호출로 직접 접근)      |     | (.sqlite) |
  +-----------------------------------+     +----------+

SQLite에서는 데이터베이스 전체가 디스크의 파일 하나입니다. 테이블도, 인덱스도, 뷰도, 트리거도 전부 그 한 파일 안에 들어 있습니다. 애플리케이션은 SQLite를 라이브러리로 링크하고, SQL 실행은 네트워크 왕복이 아니라 그냥 함수 호출입니다. 데이터베이스에 "연결"한다는 것은 그저 그 파일을 여는 것입니다.

이 단순함이 SQLite가 어디에나 있을 수 있는 근본 이유입니다. 배포할 서버가 없으니, 앱을 설치하면 데이터베이스도 함께 설치된 셈입니다. 백업은 그냥 파일 복사고, 다른 기계로 옮기는 것도 파일을 복사하면 끝입니다. 관리 부담이 사실상 0입니다.

SQLite가 클라이언트-서버 DB를 이길 때

SQLite 공식 문서에는 유명한 표현이 있습니다. "SQLite는 PostgreSQL 같은 것과 경쟁하는 것이 아니라, fopen()과 경쟁한다." 즉 SQLite의 진짜 대안은 다른 데이터베이스가 아니라, 직접 파일을 열어 읽고 쓰는 것입니다. 이 관점에서 보면 SQLite가 언제 최선인지가 분명해집니다.

SQLite가 특히 강한 경우들입니다.

  • 애플리케이션 로컬 데이터: 데스크톱·모바일 앱의 설정, 상태, 캐시, 오프라인 데이터. 사용자마다 자기 파일을 가집니다. 여기에 네트워크 DB는 과합니다.
  • 읽기가 압도적으로 많은 워크로드: SQLite는 읽기에 매우 빠릅니다. 콘텐츠가 대부분 읽히기만 하는 웹사이트라면 SQLite 하나로 놀라운 트래픽을 감당합니다.
  • 엣지와 임베디드: IoT 기기, 차량, 항공전자처럼 DB 서버를 띄울 수 없는 환경. 라이브러리로 링크되니 딱 맞습니다.
  • 파일 포맷 대체: 복잡한 자체 바이너리 포맷을 만드는 대신, SQLite 파일을 애플리케이션의 저장 포맷으로 씁니다. 스키마·트랜잭션·쿼리를 공짜로 얻습니다.
  • 테스트와 프로토타입: 뒤에서 자세히 다루겠지만, 인메모리 SQLite는 테스트에 이상적입니다.

반대로 SQLite가 부적합한 경우도 분명합니다.

  • 높은 동시 쓰기: SQLite는 쓰기를 직렬화합니다. 여러 클라이언트가 초당 수천 번씩 동시에 쓰는 워크로드라면 클라이언트-서버 DB가 낫습니다.
  • 여러 기계에서의 접근: 여러 서버가 하나의 데이터베이스에 네트워크로 붙어야 한다면, 파일 기반 모델은 맞지 않습니다.
  • 초대용량 + 복잡한 관리 기능: 세밀한 권한 관리, 복제 토폴로지, 대규모 병렬 분석 등이 필요하면 전용 DB가 낫습니다.

핵심 통찰은 이것입니다. 많은 애플리케이션이 습관적으로 클라이언트-서버 DB를 쓰지만, 실제 워크로드를 보면 SQLite로 충분하고 오히려 더 단순한 경우가 대단히 많습니다. "기본으로 무거운 DB"가 아니라 "필요할 때만 무거운 DB"로 사고를 바꾸면 시스템이 훨씬 가벼워집니다.

동시성의 핵심 — WAL 모드

SQLite에 대한 가장 흔한 오해는 "동시성이 약하다"는 것입니다. 반은 맞지만, 그 인식은 대개 오래된 기본 모드에 기반합니다. WAL(Write-Ahead Logging) 모드를 켜면 이야기가 크게 달라집니다.

전통적인 롤백 저널 모드에서는, 쓰기가 진행되는 동안 읽기가 막히고 읽기가 진행되는 동안 쓰기가 막히는 경향이 있었습니다. 데이터베이스 파일 전체에 대한 잠금이 거칠게 작동했기 때문입니다.

WAL 모드는 이 구조를 바꿉니다. 변경 사항을 데이터베이스 본 파일에 바로 쓰는 대신, 별도의 WAL 파일에 먼저 추가(append)합니다. 이 방식의 핵심 이점은 다음과 같습니다.

  • 읽기와 쓰기가 서로를 막지 않는다. 하나의 쓰기가 진행되는 동안에도 여러 읽기가 동시에 진행됩니다. 읽는 쪽은 마지막으로 커밋된 일관된 스냅샷을 보고, 쓰는 쪽은 WAL에 추가합니다.
  • 커밋이 빠르다. 변경을 WAL 끝에 순차적으로 추가하는 것은, 파일 여기저기를 무작위로 고치는 것보다 훨씬 빠릅니다.

다만 SQLite의 쓰기는 여전히 한 번에 하나입니다. WAL 모드에서도 동시에 쓰는 작성자는 한 명뿐이고, 나머지 쓰기는 순서를 기다립니다. 하지만 읽기는 그와 무관하게 병렬로 흐릅니다. 웹 애플리케이션 대부분이 "읽기 많고 쓰기 적은" 패턴이라는 점을 생각하면, WAL 모드의 이 특성은 실무에서 대단히 잘 맞습니다.

WAL 모드는 명령 한 줄로 켭니다. 한 번 설정하면 데이터베이스에 영구히 남습니다.

-- WAL 모드 활성화 (데이터베이스 파일에 영구 저장됨)
PRAGMA journal_mode = WAL;

-- 내구성과 성능의 균형을 위한 흔한 설정
PRAGMA synchronous = NORMAL;

synchronous = NORMAL은 WAL 모드에서 자주 쓰이는 조합입니다. 완전한 FULL보다 디스크 동기화를 덜 자주 해서 훨씬 빠르면서도, 정전 시 데이터베이스 손상이 없는 수준의 안전성을 유지합니다(최악의 경우 마지막 몇 개 트랜잭션만 잃을 수 있습니다). 프로덕션에서 SQLite를 쓴다면 이 두 PRAGMA는 거의 표준 설정에 가깝습니다.

이 브라우저 안에서 진짜 SQLite를 만져 보고 싶다면 SQL 놀이터에서 WAL이든 윈도우 함수든 직접 실행해 볼 수 있습니다. 분석 특화 SQL을 실험하고 싶다면 DuckDB 데이터 분석 놀이터도 함께 열어 비교해 보세요.

테스트의 비밀 병기 — 인메모리 데이터베이스

SQLite의 가장 사랑받는 실무 용도 중 하나는 테스트입니다. 파일 경로 대신 특별한 이름을 주면, SQLite는 디스크가 아니라 메모리 위에 데이터베이스 전체를 만듭니다.

import sqlite3

# 디스크가 아닌 순수 메모리 위의 데이터베이스
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("INSERT INTO users (name) VALUES ('Alice')")
row = conn.execute("SELECT name FROM users WHERE id = 1").fetchone()
print(row)  # ('Alice',)

이것이 테스트에서 강력한 이유는 여러 가지입니다.

  • 극도로 빠르다. 디스크 I/O가 전혀 없어 테스트가 순식간에 돕니다. 수천 개의 테스트가 각자 깨끗한 DB를 만들어도 부담이 없습니다.
  • 완벽한 격리. 각 테스트가 자기만의 인메모리 DB를 새로 만들면, 테스트 간 상태 오염이 원천적으로 없습니다. 테스트가 끝나면 메모리와 함께 사라집니다.
  • 설정이 필요 없다. 테스트를 돌리기 위해 별도의 데이터베이스 서버를 띄우거나 정리할 필요가 없습니다. CI 환경에서 특히 값집니다.

한 가지 주의점은 프로덕션과 테스트의 데이터베이스가 다를 때 생기는 미묘한 차이입니다. 프로덕션에서 PostgreSQL을 쓰면서 테스트만 인메모리 SQLite로 하면, SQL 방언 차이 때문에 테스트는 통과하지만 프로덕션에서 깨지는 경우가 생길 수 있습니다. 그래서 가장 안전한 조합은 프로덕션도 SQLite를 쓰는 것이고, 그게 아니라면 최소한 통합 테스트만큼은 실제와 같은 DB로 돌리는 것입니다.

브라우저 속 SQLite — WASM이라는 새 지평

SQLite의 이식성은 원래도 대단했지만, WebAssembly는 그것을 완전히 새로운 곳으로 데려갔습니다. 바로 브라우저 안입니다.

SQLite는 잘 짜인 C 코드베이스라서 WASM으로 컴파일하기에 이상적입니다. 그 결과 이제 브라우저 탭 안에서, 서버 없이, 진짜 SQLite 엔진이 돕니다. 흉내가 아니라 실제 SQLite입니다. 앞서 링크한 SQL 놀이터가 바로 이 방식으로 동작합니다.

이 조합이 열어주는 가능성은 상당합니다.

  • 완전한 클라이언트 사이드 앱: 데이터가 브라우저를 떠나지 않는 오프라인 우선 웹 애플리케이션을 만들 수 있습니다. 사용자 데이터를 브라우저의 지속 저장소에 SQLite 파일로 보관합니다.
  • 프라이버시: 민감한 데이터를 서버에 보내지 않고 브라우저에서 직접 SQL로 분석합니다.
  • 교육과 도구: SQL을 가르치거나 데이터를 탐색하는 도구를, 백엔드 인프라 없이 순수 정적 사이트로 배포할 수 있습니다.

PostgreSQL 계열도 WASM으로 오는 흐름이 있지만, SQLite는 애초에 임베디드 라이브러리로 설계되었기 때문에 이 전환이 가장 자연스럽습니다. "데이터베이스를 앱 안에 넣는다"는 SQLite의 원래 철학이, 브라우저라는 새로운 앱 환경에서 그대로 이어진 셈입니다.

내구성과 복제 — litestream이라는 우아한 해법

SQLite를 프로덕션 웹 서버에서 쓸 때 가장 먼저 나오는 걱정은 이것입니다. "데이터베이스가 서버의 로컬 파일 하나인데, 그 서버가 죽으면 데이터는 어떻게 되나?"

전통적인 답은 클라이언트-서버 DB의 복제 기능을 쓰는 것이었습니다. 하지만 SQLite 생태계에는 더 우아한 접근이 있습니다. litestream 같은 도구입니다.

litestream의 아이디어는 영리합니다. 앞서 설명한 WAL 파일을 기억하시나요? litestream은 이 WAL에 추가되는 변경 사항을 실시간으로 관찰하면서, 그것을 S3 같은 객체 저장소로 연속 스트리밍합니다. 즉 데이터베이스에 일어나는 모든 변경이 거의 실시간으로 외부에 백업되는 것입니다.

  애플리케이션
      |
      v (쓰기)
  SQLite (로컬 파일 + WAL)
      |
      v (litestream이 WAL 변경을 연속 관찰)
  객체 저장소 (S3 등)로 스트리밍 백업
      |
      v (장애 시)
  다른 서버에서 최신 상태로 복원

이 방식의 이점은 다음과 같습니다.

  • 거의 실시간 백업. 정기 스냅샷과 달리, 변경이 발생하는 즉시 외부로 흘러갑니다. 서버가 죽어도 잃는 데이터가 최소화됩니다.
  • 간단한 복원. 새 서버에서 객체 저장소로부터 데이터베이스를 복원하면 마지막 상태에 가깝게 되살아납니다.
  • 저렴하고 단순함. 별도의 복제 서버 클러스터를 운영할 필요 없이, 값싼 객체 저장소 하나면 됩니다.

이 접근은 "SQLite는 내구성이 약하다"는 통념을 뒤집습니다. 단일 파일의 단순함을 유지하면서도, 클라우드 객체 저장소를 백업 대상으로 삼아 견고한 내구성을 얻습니다. 최근에는 여러 서버가 SQLite를 공유하거나 읽기 복제본을 두는 방향으로 생태계가 더 발전하고 있어서, "SQLite는 단일 서버용"이라는 인식마저 조금씩 바뀌고 있습니다.

SQLite를 제대로 쓰기 위한 실무 체크리스트

지금까지의 내용을 프로덕션 관점의 실무 지침으로 압축하면 다음과 같습니다.

  • WAL 모드를 켜라. 웹 애플리케이션이라면 거의 항상 옳습니다. PRAGMA journal_mode = WALPRAGMA synchronous = NORMAL은 사실상 기본 설정입니다.
  • 쓰기는 직렬화됨을 기억하라. SQLite는 동시 쓰기를 하나로 직렬화합니다. 쓰기 트랜잭션은 짧게 유지하고, 긴 트랜잭션 안에서 오래 붙잡지 마세요.
  • 바쁜 대기 시간(busy timeout)을 설정하라. 쓰기 잠금이 잡혀 있을 때 즉시 실패하는 대신 잠깐 기다리도록 busy_timeout을 설정하면 순간적인 경합을 부드럽게 넘깁니다.
  • 테스트는 인메모리로. 빠르고 격리된 테스트를 위해 :memory: 데이터베이스를 활용하세요. 다만 프로덕션 DB와의 방언 차이에 주의하세요.
  • 내구성은 litestream 같은 도구로. 프로덕션 웹에서 쓴다면 WAL 스트리밍 백업으로 데이터 손실 위험을 낮추세요.
  • 백업은 파일 복사가 아니라 올바른 방법으로. 실행 중인 데이터베이스를 단순 cp로 복사하면 손상될 수 있습니다. SQLite의 백업 API나 VACUUM INTO, 혹은 litestream을 쓰세요.

마치며

SQLite는 소프트웨어 공학의 흔치 않은 성공 사례입니다. 화려하지 않고, 마케팅도 조용하지만, 지구상에서 가장 널리 쓰이는 데이터베이스가 되었습니다. 그 비결은 급진적인 단순함입니다. 서버를 없애고 데이터베이스를 파일 하나로 만든 결정이, SQLite를 폰과 브라우저와 비행기 안으로 데려갔습니다.

그리고 그 단순함은 약점이 아닙니다. WAL 모드가 동시성 문제를 상당 부분 풀고, 인메모리 모드가 테스트를 혁신하고, WASM이 브라우저라는 새 무대를 열고, litestream이 내구성 걱정을 해소하면서, SQLite는 "장난감 DB"라는 낡은 인식을 완전히 벗어났습니다. 진지한 프로덕션 시스템의 진지한 선택지가 되었습니다.

다음에 새 프로젝트에서 "일단 PostgreSQL을 띄우자"고 반사적으로 생각하게 되거든, 잠깐 멈추고 물어보세요. "이 워크로드, SQLite로 충분하지 않나?" 놀랍도록 자주, 답은 "그렇다"입니다. 그리고 그 선택은 여러분의 시스템을 훨씬 단순하고 견고하게 만들 것입니다.

참고 자료

현재 단락 (1/94)

데이터베이스 이야기를 하면 사람들은 보통 PostgreSQL, MySQL, Oracle, 요즘이면 클라우드 데이터 웨어하우스를 떠올립니다. 그런데 지구상에서 가장 널리 배포된 데이...

작성 글자: 0원문 글자: 6,511작성 단락: 0/94