Claude Code의 동시 세션 문제 해결: SQLite WAL 모드로 메모리 MCP 구현

발행: (2026년 1월 1일 오후 05:55 GMT+9)
9 min read
원문: Dev.to

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은 언제나 환영합니다.

Back to Blog

관련 글

더 보기 »

SQLite에서 캐시 효율성

SQLite에서 캐시 효율성 !표지 이미지: A Eficiência do Cache no SQLite https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=aut...