Claude Code의 동시 세션 문제 해결: SQLite WAL 모드로 메모리 MCP 구현
Source: Dev.to
문제
여러 Claude Code 세션을 실행하면서 다음 오류를 본 적이 있나요?
Error: database is locked
네, 저도 처음엔 “뭐, 진짜야?” 라는 반응을 보였어요.
Memory MCP는 Claude Code 세션 간에 지식을 공유할 수 있는 매우 강력한 도구이지만, 공식 구현은 동시 접근을 지원하지 않습니다. 그래서 저는 SQLite의 Write‑Ahead Logging (WAL) 모드를 사용해 이 문제를 한 번에 해결하는 버전을 만들었습니다.
대상은 누구인가요?
- 일상 업무 흐름에서 Claude Code를 사용하는 개발자
- Model Context Protocol (MCP)에 관심이 있는 모든 사람
- 여러 AI 세션을 병렬로 실행하고자 하는 팀
- 실용적인 SQLite 구현 패턴을 찾는 개발자
공식 Memory MCP가 부족한 이유
공식 Memory MCP (@modelcontextprotocol/server-memory)는 지식 그래프를 JSONL 파일에 저장합니다. 이 접근 방식에는 몇 가지 심각한 제한 사항이 있습니다:
// Simplified view of the official implementation
const data = await fs.readFile('memory.jsonl', 'utf-8');
// ... process data ...
await fs.writeFile('memory.jsonl', newData);
- 파일 잠금 없음 – 여러 세션에서 동시에 쓰면 데이터가 손상될 수 있습니다.
- 선형 검색 – 구현이 모든 데이터를 메모리로 읽어들입니다; 그래프가 커질수록 쿼리가 느려집니다.
- 원자적 업데이트 없음 – 부분적인 실패가 파일을 일관성 없는 상태로 남겨, 프로덕션 사용에 큰 장애가 됩니다.
내 솔루션: SQLite 기반 메모리 MCP
SQLite의 WAL 모드는 정말로 판도를 바꾸는 기능입니다:
| 기능 | WAL이 제공하는 것 |
|---|---|
| 동시 읽기/쓰기 | 여러 읽기 작업 + 하나의 쓰기 작업이 동시에 DB에 접근할 수 있습니다 |
| 충돌 복구 | 프로세스가 쓰기 중에 충돌하더라도 데이터 무결성이 보장됩니다 |
| 빠른 쓰기 | 로그 기반 접근 방식이 기존 롤백 저널보다 성능이 뛰어납니다 |
이것은 모든 것을 바꾸는 “간단한” 기능 중 하나입니다.
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
export class KnowledgeGraphStore {
private db: Database.Database;
constructor(dbPath: string) {
// Create directory if it doesn’t exist
const dir = path.dirname(dbPath);
if (dir && dir !== '.') {
fs.mkdirSync(dir, { recursive: true });
}
this.db = new Database(dbPath);
// Enable WAL mode for concurrent access
this.db.pragma('journal_mode = WAL');
// Set busy timeout to wait 5 seconds on lock contention
this.db.pragma('busy_timeout = 5000');
this.initSchema();
}
}
핵심 포인트
journal_mode = WAL– 동시 읽기/쓰기 접근을 가능하게 합니다.busy_timeout = 5000– 잠금 충돌 시 오류를 발생시키기 전에 5초 동안 대기합니다.
busy timeout을 설정하지 않으면 즉시 잠금 오류가 발생합니다. 꼭 설정하세요.
스키마 설계
효율적인 관리를 위해 지식 그래프를 세 개의 테이블로 구조화했습니다:
private initSchema(): void {
this.db.exec(`
-- Entities (concepts) management
CREATE TABLE IF NOT EXISTS entities (
name TEXT PRIMARY KEY,
entity_type TEXT NOT NULL
);
-- Observations and facts about entities
CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_name TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (entity_name) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(entity_name, content) -- Prevent duplicates
);
-- Relationships between entities
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_entity TEXT NOT NULL,
to_entity TEXT NOT NULL,
relation_type TEXT NOT NULL,
FOREIGN KEY (from_entity) REFERENCES entities(name) ON DELETE CASCADE,
FOREIGN KEY (to_entity) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(from_entity, to_entity, relation_type) -- Prevent duplicates
);
-- Indexes for performance optimization
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_name);
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
`);
}
설계 원칙
| 원칙 | 왜 중요한가 |
|---|---|
| CASCADE | 엔터티가 삭제될 때 관련 데이터를 자동으로 삭제합니다 (고아 레코드가 남지 않음). |
| UNIQUE constraints | 중복 데이터를 방지합니다. |
| Indexes | 쿼리 성능을 크게 향상시킵니다. 이 인덱스들은 매우 중요합니다 – 없으면 쿼리가 급격히 느려집니다. |
데이터 삽입 – 트랜잭션이 중요합니다
createEntities(entities: Entity[]): Entity[] {
const insertEntity = this.db.prepare(
'INSERT OR IGNORE INTO entities (name, entity_type) VALUES (?, ?)'
);
const insertObservation = this.db.prepare(
'INSERT OR IGNORE INTO observations (entity_name, content) VALUES (?, ?)'
);
const created: Entity[] = [];
// Process everything in a transaction
const transaction = this.db.transaction((entities: Entity[]) => {
for (const entity of entities) {
insertEntity.run(entity.name, entity.entityType);
for (const obs of entity.observations) {
insertObservation.run(entity.name, obs);
}
created.push(entity);
}
});
transaction(entities);
return created;
}
왜 트랜잭션을 사용하나요?
- 원자성 – 실패한 작업이 부분적인 변경을 남기지 않음.
- 일관성 – 다중 테이블 업데이트가 참조 무결성을 유지함.
- 성능 – 배치 커밋이 훨씬 빠름.
트랜잭션 없이 오류가 발생하면 데이터가 손상될 수 있습니다 – 저는 이를 직접 겪으며 배웠습니다.
시작하기
I published this as an npm package so anyone can use it easily:
npm install @pepk/mcp-memory-sqlite
Add the server to your ~/.claude.json (or global config):
{
"mcpServers": {
"memory": {
"command": "npx",
"args": ["@pepk/mcp-memory-sqlite"],
"env": {
"MEMORY_DB_PATH": "./.claude/memory.db"
}
}
}
}
설정 팁
| 변수 | 권장 사항 |
|---|---|
MEMORY_DB_PATH | ./.claude/memory.db와 같은 프로젝트별 데이터베이스가 관리하기 더 쉽습니다. |
| 전역 공유 | 모든 프로젝트에서 하나의 DB가 필요하면 ~/memory.db(또는 유사 경로)로 설정하세요. |
기능 비교
| 기능 | 공식 (JSONL) | 이 구현 (SQLite + WAL) |
|---|---|---|
| 동시 접근 | ❌ 지원되지 않음 | ✅ 지원됨 (WAL) |
| 트랜잭션 | ❌ 없음 | ✅ ACID 보장 |
| 검색 속도 | 느림 (선형) | 빠름 (인덱스) |
데이터 무결성
| 수준 | 설명 |
|---|---|
| 약함 | – |
| 강함 | (foreign keys) |
Source: …
충돌 복구
| 기능 | 상태 |
|---|---|
| 수동 복구 | ❌ |
| 자동 (WAL) | ✅ |
차이점은 상당히 큽니다.
“database is locked”
WAL 모드를 사용 중에도 이 오류가 계속 발생한다면 다음을 시도해 보세요:
// busy timeout 늘리기
this.db.pragma('busy_timeout = 10000'); // 10초
5초가 충분하지 않다면 10초로 늘려 보세요.
WAL 파일을 잘라내기 위해 체크포인트 실행
sqlite3 memory.db "PRAGMA wal_checkpoint(TRUNCATE);"
이 명령을 주기적으로 실행하면 WAL 파일 크기를 제어할 수 있습니다.
Source: …
메모리 MCP 구현 (SQLite WAL)
SQLite의 WAL 모드를 사용하여 Memory MCP 구현을 만들었으며, 이를 통해 동시 세션 문제를 해결했습니다.
핵심 요점
- WAL 모드는 동시 읽기/쓰기 접근을 가능하게 합니다.
- 트랜잭션은 데이터 일관성을 보장합니다.
- 인덱스를 사용해 빠른 검색 성능을 제공합니다.
WAL 모드의 동시 접근 지원이 여기서 진정한 게임 체인저입니다. 이제 데이터 손상에 대한 걱정 없이 여러 세션을 안전하게 실행할 수 있습니다.
npm 패키지
@pepk/mcp-memory-sqlite
저장소
문서 (일본어)
- Qiita: 일본어로 읽기
- Zenn: Zenn에서 읽기
- note (Story): 개발 스토리
저자 소개
Daichi Kudo
- Cognisant LLC의 CEO – 인간과 AI가 함께 창조하는 미래를 구축합니다
- M16 LLC의 CTO – AI, 창의성 및 엔지니어링
이것이 Claude Code 동시 세션 문제 해결에 도움이 된다면 알려 주세요! 이슈와 PR은 언제나 환영합니다.