Monlite – 하나의 SQLite 파일로 구현하는 문서, 벡터, 캐시 및 작업 큐
요약
Monlite는 SQLite를 기반으로 문서 저장, 벡터 검색, 캐시, 작업 큐 등을 하나의 파일로 통합 구현한 TypeScript 라이브러리입니다. 복잡한 Docker 컨테이너 설정 없이 로컬 AI 에이전트 개발을 위한 경량 인프라를 구축할 수 있게 해줍니다.
핵심 포인트
- SQLite 기반의 단일 파일로 벡터 검색, 캐시, 작업 큐 통합 제공
- Docker 없이 로컬 AI 에이전트 인프라를 즉시 구축 가능
- sqlite-vec 확장을 통한 KNN 쿼리 및 벡터 검색 지원
- BEGIN IMMEDIATE를 활용한 원자적 작업 할당으로 경합 조건 해결
제가 시작하는 모든 로컬 AI 에이전트 프로젝트는 항상 같은 방식으로 시작됩니다. 에이전트 코드가 아니라 인프라부터 시작하죠. 메모리를 위한 MongoDB, 캐시와 잠금(locks)을 위한 Redis, 벡터를 위한 Qdrant, 작업 큐(task queue)를 위한 BullMQ까지. 한 시간이 지나도 아직 애플리케이션 로직은 단 한 줄도 작성하지 못한 상태가 됩니다.
규모가 커졌을 때 이러한 도구들이 잘못된 것은 아닙니다. 하지만 아이디어를 로컬에서 테스트하기 위해 단지 5개의 Docker 컨테이너를 실행하는 것은 매 실험마다 지불해야 하는 세금처럼 느껴지기 시작했습니다. 그래서 저는 질문을 던지기 시작했습니다. '만약 SQLite가 이 모든 것을 할 수 있다면 어떨까?'
그 질문이 Monlite가 되었습니다. Monlite는 하나의 .db 파일이 문서 저장소(document store), 벡터 검색(vector search), 전체 텍스트 검색(full-text search), 키-값 캐시(key-value cache), 작업 큐(job queue), 그리고 cron 스케줄러까지 로컬 스택 전체를 아우르는 TypeScript 라이브러리입니다. 모든 것이 동일한 파일과 동일한 연결을 공유합니다. Docker도 필요 없고, 서비스들을 서로 연결하는 글루 코드(glue code)도 필요 없으며, "올바른 순서로 시작하기"를 고민할 필요도 없습니다.
const db = createDb("./agent.db")
const store = createVectorStore(db)
const queue = createQueue(db)
...
기반은 SQLite가 이미 잘 수행하고 있는 기능들입니다. ACID 트랜잭션(ACID transactions), 내구성을 위한 WAL 모드, 엔진에 내장된 FTS5가 그것입니다. 벡터 검색은 sqlite-vec 확장 기능에서 제공됩니다. 이는 KNN 쿼리를 처리하는 vec0 가상 테이블(virtual table) 타입을 추가합니다. 문서 API는 json_extract()와 그 위의 TypeScript 레이어로 구성되며, 타입이 지정된 컬렉션(typed collections)을 사용할 때 반환 타입을 좁혀주는 Mongo/Prisma 스타일의 where/orderBy를 지원합니다.
가장 구현하기 오래 걸렸던 부분은 여러 워커 프로세스(worker processes)에 걸쳐 정확히 한 번만 작업이 할당되도록(exactly-once job claiming) 만드는 것이었습니다. 대기 중인 작업을 읽고 나서 활성 상태로 업데이트하는 단순한 방식은 여러 워커가 동일한 파일을 공유할 때 경합 조건(race condition)이 발생합니다. 처음에는 낙관적 잠금(optimistic locking)을 시도해 보았지만 재시도 로직이 복잡했습니다. 진짜 해답은 더 간단했습니다: BEGIN IMMEDIATE. SQLite의 쓰기 의도 잠금(write-intent lock)은 읽기와 할당을 하나의 원자적 단계(atomic step)로 만들어주며, 전체 과정은 다음과 같이 단순화됩니다:
const job = await jobs.findOneAndUpdate({
where: { status: "pending", type: "summarize" },
data: { $set: { status: "active" }, $inc: { version: 1 } },
...
만약 다른 프로세스가 당신보다 먼저 선점했다면, null을 반환받게 됩니다. 단일 작업을 두고 경쟁하는 8개의 동시 워커(concurrent workers)를 대상으로 테스트했을 때, 매번 정확히 단 하나만이 승리했습니다. 이는 Redis와 BullMQ가 제공하는 것과 동일한 보장(guarantee)이며, 단지 네트워크 소켓(network socket) 대신 디스크 상의 파일을 사용할 뿐입니다.
계획하지 않았지만 정말 유용하게 작용하게 된 점이 있습니다. 바로 Python에서도 동일한 .db 파일을 읽을 수 있다는 것입니다. 포맷이 문서화된 컨벤션(conventions)을 따르는 일반적인 SQLite이기 때문에 — 독점적인 인코딩(proprietary encoding)이나 와이어 프로토콜(wire protocol) 없이 — Python 포트에서도 파일을 열어 직접 쿼리할 수 있습니다. 따라서 Python이 무거운 작업(청킹(chunking), 임베딩(embedding), 인제스션(ingesting))을 수행하고 Node가 서빙(serving) 측을 담당하면서, 두 환경 사이에 별도의 변환 계층(translation layer) 없이 하나의 파일을 공유하는 방식이 일반적인 패턴이 될 수 있습니다.
db = create_db("agent.db") # Node가 쓰고 있는 것과 동일한 파일
db.collection("docs").find_many(where={"status": "ready"})
kv(db).set_nx("lock:job:42", 1, ttl=5_000)
우리는 라운드 트립(round-trip) 테스트 스위트를 통해 상호 운용성(interop)을 검증합니다. Node에서 쓰고, Python에서 읽고, Python에서 쓰고, Node에서 읽는 방식입니다. 스키마 컨벤션(schema conventions)이 문서화되어 있으므로 어떤 런타임(runtime)이든 참여할 수 있습니다.
Node >= 22.5 버전에서는 네이티브 의존성(native dependencies) 없이 내장된 node:sqlite 위에서 코어가 실행됩니다. Node 18/20 버전의 경우 better-sqlite3를 설치하면 자동으로 인식됩니다. 어떤 경우든 인터페이스와 테스트는 동일합니다.
한계점에 대해서도 명확히 말씀드려야겠습니다. SQLite는 단일 쓰기(single-writer) 방식입니다. 로컬 에이전트 워크로드(local agent workloads)에는 훌륭하지만, 초당 수천 건의 동시 쓰기가 발생하는 환경에는 적합하지 않습니다. 반응형 watch()는 프로세스 내부(in-process)에서 작동하며, 별도의 프로세스 간에 자동으로 트리거되지는 않습니다. 또한, 이 시스템은 의도적으로 분산 시스템(distributed system)으로 설계되지 않았습니다. 클라우드가 필요한 경우 @monlite/sync를 통해 MongoDB, Postgres 또는 MySQL로 복제할 수 있지만, Monlite의 진정한 홈은 단일 머신(single machine)입니다.
핵심(core)은 API가 고정된(frozen) v2.6.1 버전입니다. 별도의 선택적(opt-in) 패키지들이 벡터(vectors), 전체 텍스트 검색(FTS), 캐시(cache), 큐(queue), 크론(cron), 동기화(sync), 브라우저(SQLite-WASM을 통해), 그리고 Electron을 지원합니다. Python 포트(port)는 현재 문서(documents)와 키-값(kv) 저장 기능을 제공하며, 나머지는 개발 진행 중입니다.
npm install @monlite/core
GitHub: https://github.com/qataruts/monlite
구현 방식에 대해 함께 이야기 나누는 것을 환영합니다 — sqlite-vec 연결 방식, 플러그인 시스템이 afterWrite를 통해 어떻게 FTS와 벡터 인덱스(vector indexes)를 동기화하는지, 동기화 엔진(sync engine)의 LWW(Last-Write-Wins) 충돌 해결 방식, 또는 왜 CAS(Compare-And-Swap) 문제를 해결하는 데 낙관적 잠금(optimistic locking)보다 BEGIN IMMEDIATE가 더 효과적이었는지 등에 대해 논의할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기