
【#10】 OpenClaw 분석하기 — 모든 것은 SQLite로 귀결된다, 상태의 저장소
요약
OpenClaw의 상태 관리 아키텍처를 분석하며, 모든 상태를 SQLite로 집약하는 설계 원칙을 다룹니다. 글로벌 상태와 에이전트 단위 상태를 분리하여 관리하는 방식과 보안을 위한 파일 권한 설정, Kysely를 활용한 데이터베이스 접근 방식을 설명합니다.
핵심 포인트
- 상태 관리를 위해 공유 DB와 에이전트 단위 DB로 이원화하여 운영
- 데이터 산재를 방지하기 위한 엄격한 저장소 선택 규율 적용
- 인증 정보 보호를 위해 SQLite 파일에 엄격한 파일 권한(0600) 설정
- Raw SQL 대신 Kysely를 사용하여 타입 안정성 및 유지보수성 확보
본 기사의 코드 참조는 OpenClaw main의 cee2aca409 (version 2026.6.10) 시점입니다. 행 번호는 업데이트에 따라 어긋날 수 있습니다.
연재 「OpenClaw 분석하기」
#01에서 "상태는 SQLite로 집약한다", "Kysely를 사용하여 생 SQL (raw SQL)을 피한다", "마이그레이션은 database-first로 한다"라는 3가지 원칙을 예고했습니다. 이번에는 그 구현을 src/state/, src/plugin-state/, src/infra/로부터 분석합니다. 거대 애플리케이션의 "데이터 저장소"를 어떻게 일관되게 유지하고 있는지에 대한 내용입니다.
OpenClaw의 런타임 상태는 2개의 SQLite로 나뉩니다.
공유 상태 DB (글로벌 상태 + 플러그인 KV). 경로는 src/state/openclaw-state-db.paths.ts:40입니다.
export function resolveOpenClawStateSqlitePath(env = process.env): string {
return path.join(resolveOpenClawStateSqliteDir(env), "openclaw.sqlite");
}
실체는 ~/.openclaw/state/openclaw.sqlite입니다.
에이전트 단위 DB (에이전트 스코프의 상태/캐시). 경로는 src/state/openclaw-agent-db.paths.ts:20입니다.
export function resolveOpenClawAgentSqlitePath(options): string {
const agentId = normalizeAgentId(options.agentId);
return path.resolve(
...
실체는 ~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite입니다.
AGENTS.md의 구분 규칙은 다음과 같습니다.
Use the shared state DB (state/openclaw.sqlite) for global runtime state and plugin KV data. Use the per-agent DB (agents/<agentId>/agent/openclaw-agent.sqlite) for agent-scoped state/cache. Use a dedicated SQLite DB only when schema, volume, or lifecycle clearly does not fit those stores.
"글로벌은 공유 DB, 에이전트 고유는 에이전트 DB, 둘 다에 맞지 않는 경우에만 전용 DB". 새로운 데이터의 저장소를 고민된다면, 우선 이 두 가지 중 하나를 선택합니다. 이것이 상태의 산재를 방지하는 단순하고 강력한 규율입니다.
파일은 엄격한 퍼미션 (permission)으로 생성됩니다 (src/state/openclaw-state-db.ts:37).
const OPENCLAW_STATE_DIR_MODE = 0o700;
const OPENCLAW_STATE_FILE_MODE = 0o600;
디렉토리는 0700, 파일은 0600입니다. 상태 DB는 인증 정보나 세션을 포함할 수 있으므로, 소유자만 읽을 수 있는 권한으로 고정합니다. WAL/SHM/journal과 같은 사이드카 (sidecar) 처리는 상시 런타임이 아니라 구형 스토어의 이관 시에만 필요하기 때문에, 마이그레이션 측에 집약되어 있습니다 (src/infra/state-migrations.ts의 PLUGIN_STATE_SQLITE_SIDECAR_SUFFIXES = ["", "-shm", "-wal", "-journal"] 등, 사이드카 4종을 다룸).
AGENTS.md의 규약.
SQLite runtime access uses Kysely helpers, not raw SQL statement strings, except schema DDL, migrations, low-level DB bootstrap, or narrowly justified SQLite primitives.
Kysely는 타입 안전한 (Type-safe) 쿼리 빌더 (Query builder)입니다. 헬퍼 함수들은 src/infra/kysely-sync.ts에 집약되어 있습니다.
export function getNodeSqliteKysely<Database>(db: DatabaseSync): Kysely<Database> {
const existing = kyselyByDatabase.get(db);
if (existing) return existing as Kysely<Database>;
...
핵심은 CompileOnlyNodeSqliteKyselyDialect입니다. — Kysely를 쿼리 컴파일(SQL 문자열 + 파라미터 생성)에만 사용하고, 실행은 Node 표준의 node:sqlite (DatabaseSync)를 통해 동기적으로 수행하는 하이브리드 방식입니다. Kysely 인스턴스는 DB별로 캐싱됩니다.
대표적인 쿼리 예시(스키마 메타데이터의 upsert, src/state/openclaw-state-db.ts:835).
const kysely = getNodeSqliteKysely<OpenClawStateMetadataDatabase>(db);
executeSqliteQuerySync(db,
kysely.insertInto("schema_meta")
...
타입이 지정된 insertInto(...).values(...).onConflict(...)를 통해, 문자열 연결 없이 UPSERT를 작성할 수 있습니다. SQL 인젝션(SQL Injection)의 틈이 없으며, 테이블 이름과 컬럼 이름이 타입 체크(Type check)되는 점이 매우 유용합니다.
플러그인의 영속 데이터(Persistent data)는 공유 DB의 plugin_state_entries 테이블에 저장됩니다 (src/state/openclaw-state-schema.sql).
CREATE TABLE IF NOT EXISTS plugin_state_entries (
plugin_id TEXT NOT NULL,
namespace TEXT NOT NULL,
...
(plugin_id, namespace, entry_key)의 복합 기본 키(Composite primary key)를 사용하여 플러그인별, 네임스페이스(Namespace)별로 분리됩니다. expires_at을 통해 TTL(Time To Live)을 가질 수 있습니다. 조작 함수(src/plugin-state/plugin-state-store.sqlite.ts)는 register/upsert, lookup, consume(읽은 즉시 삭제), entries(목록), sweep(TTL 만료 데이터 정리)으로 구성됩니다. 읽기 작업은 TTL을 고려합니다.
.where("plugin_id", "=", params.pluginId)
.where("namespace", "=", params.namespace)
.where("entry_key", "=", params.key)
...
"만료 기한이 없거나 미래에 만료되는" 항목만 반환합니다. 쓰기 작업은 runOpenClawStateWriteTransaction()을 통해 ACID를 보장하는 트랜잭션(Transaction) 내에서 수행됩니다. 플러그인은 #04의 SDK를 통해 이 KV(Key-Value)를 사용하며, 자체적인 파일을 생성하지 않습니다. 이는 "플러그인의 스크래치 데이터도 SQLite로 관리한다"는 AGENTS.md의 철저한 준수입니다.
가장 특징적인 것은 마이그레이션(Migration) 철학입니다. AGENTS.md 내용:
State/storage migrations are database-first. Runtime reads/writes the canonical store only. Old file stores, sidecars, aliases, and fallback readers belong in openclaw doctor --fix migration code only, never steady-state runtime.
즉, 런타임(Runtime)은 정준 스토어(Canonical store, 현재의 SQLite)만 접한다는 뜻입니다. 오래된 파일 스토어나 사이드카(Sidecar)를 읽는 코드는 오직 openclaw doctor --fix 마이그레이션 코드에만 존재해야 하며, 정상 상태의 런타임에는 절대 두지 않습니다.
실례: 레거시 플러그인 상태 사이드카 (plugin-state/state.sqlite)로부터의 이행 (src/infra/state-migrations.ts:352).
function readLegacyPluginStateSidecarRows(sourcePath: string): LegacyPluginStateSidecarRow[] {
const sqlite = requireNodeSqlite();
const db = new sqlite.DatabaseSync(sourcePath, { readOnly: true });
...
여기서는 생 SQL (Raw SQL)을 사용하고 있다는 점에 주목하십시오. AGENTS.md에서 "마이그레이션은 Kysely의 예외"라고 명시하고 있듯이, 레거시 읽기는 마이그레이션 (migration) 코드의 특권입니다. 읽어들인 행은 Kysely를 통해 공유 DB에 onConflict(...).doNothing()으로 투입되며, 기존 파일은 .migrated 접미사(suffix)와 함께 아카이브됩니다.
스키마 버전은 OPENCLAW_STATE_SCHEMA_VERSION (src/state/openclaw-state-db.ts:34)에서 관리하며, PRAGMA user_version에 저장합니다. 새로운 컬럼 추가는 가산적 (additive) 방식이며, ensureAdditiveStateColumns()가 대응하고 버전 범프 (version bump)를 동반하지 않습니다. 파괴적 복구 (복합 기본 키의 교정 등)는 doctor 로직 내에 격리됩니다.
이 장의 규율은 언뜻 보면 오버엔지니어링 (over-engineering)처럼 보일 수 있습니다. 하지만 #01의 "정준 경로는 하나다"라는 내용과 연결하면 납득이 갑니다.
- 런타임에 "만약 새 형식이 없다면 이전 형식을 읽는다"라는 분기를 만들지 않기 때문에, 상태 읽기 코드가 하나로 유지됩니다.
- 호환성 책임을 doctor에게 일극 집중시키기 때문에, "언제, 어디서, 무엇을 이행했는지"를 추적할 수 있습니다.
- 모든 것이 SQLite이므로, 백업·검사·이행이 단일 메커니즘으로 해결됩니다.
AGENTS.md의 "Cache/transient state gets no compat migration ... Prefer delete/drop/rebuild over import. (캐시/일시적 상태는 호환 마이그레이션을 수행하지 않으며, 가져오기보다 삭제/드롭/재구축을 우선한다)"라는 결단 또한 이 철학의 연장선입니다. 잃어버려도 상관없는 상태는 이행하지 않고 버린 뒤 다시 만듭니다.
- 상태는 이층 SQLite (2-layer SQLite) (공유 DB / 에이전트 DB)로 집약. 엄격한 파일 권한 적용.
- 쿼리는 Kysely로 컴파일 +. 생 SQL은 DDL/마이그레이션 등의 예외 상황에서만 사용.
node:sqlite로 동기 실행. - 플러그인 KV는 공유 DB의
plugin_state_entries(복합 기본 키 + TTL). 플러그인은 자체 파일을 만들지 않음. - 마이그레이션은 database-first: 런타임은 정준 저장소만을 바라보며, 호환성은 doctor에 일극 집중.
openclaw doctor --fix.
#11은 지금까지 다루지 않았던 주변부이지만 중요한 영역인, **미디어 생성·이해와 외부 프로토콜 (MCP / ACP)**입니다. 이미지·영상·음성의 생성과 이해를 어떻게 기능 (capability)으로 묶을 것인지, 그리고 MCP 서버/클라이언트와 ACP (Agent Client Protocol)를 통해 외부 도구 및 외부 에이전트와 어떻게 연결되는지를 파헤쳐 봅니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기