서버 없이 에이전트 메모리를 여러 머신에서 공유하는 방법 (또는 병합 충돌 방지)
요약
기존 에이전트 메모리 서버는 단일 JSONL 파일에 모든 지식 그래프를 저장하여 다중 머신 환경에서 동기화 시 쓰기 손실 및 컨텍스트 손실 문제가 발생했습니다. 이를 해결하기 위해 'server-noonien'이라는 CRDT 기반의 분산 스토어를 개발했습니다. 이 방식은 각 노드가 자신의 샤드에만 추가하고, 읽을 때 모든 샤드를 폴딩하여 충돌 없이 수렴하는 것이 핵심입니다.
핵심 포인트
- CRDT를 활용하여 다중 머신 환경에서 에이전트 메모리 동기화 문제를 해결함.
- 데이터 저장을 단일 파일 대신 노드별 전용 로그(append-only log)로 분할하고 샤드를 사용합니다.
- LWW-Element-Set과 하이브리드 논리 클록을 사용하여 시간 순서에 관계없이 안정적인 데이터 수렴성을 보장합니다.
- 멱등성, 교환법칙 등 수학적 속성에 기반하여 충돌 없이 여러 노드가 동일한 그래프를 형성할 수 있습니다.
우리는 코딩 에이전트를 워크스테이션, 노트북, 소형 서버 등 한 대 이상의 머신에서 실행하는데, 호스트를 전환할 때마다 에이전트가 무언가를 잊어버리는 문제가 있었습니다. 이 문제를 해결하려던 과정에서 MCP 메모리 서버의 저장 모델을 접하게 되었고, 그 결과 server-noonien으로 발표된 작은 CRDT 스토어를 개발하게 되었습니다.
문제점
공식 MCP 메모리 서버(@modelcontextprotocol/server-memory)는 전체 지식 그래프를 단일 JSONL 파일에 저장하며, 모든 호출이 이 파일을 통째로 읽고 다시 씁니다. 한 대의 머신에서는 문제가 없지만, 두 대 이상의 머신이 이를 공유하려 하면 근본적으로 잘못된 구조입니다:
- 한 머신에 존재하기 때문에 호스트를 전환하면 컨텍스트가 손실됩니다.
- 동기화하는 폴더(Syncthing, Dropbox, git 작업 트리)에 두면 쓰기 손실이 발생합니다. 이는 두 작성자가 전체 파일 읽기-수정-쓰기를 하는 과정이 안전하지 않기 때문입니다.
- 이를 피하는 대안은 호스팅 서비스뿐인데, 이 경우 메모리가 다른 사람의 클라우드 계정에 의존하게 됩니다.
아이디어: 도구는 같고 저장소만 다르게
server-noonien은 공식 서버에 드롭인(drop-in) 방식으로 적용됩니다. 동일한 아홉 가지 도구, 동일한 입력 및 출력을 사용하므로 에이전트에서 memory 서버 항목만 교체하면 다른 부분은 아무것도 변경할 필요가 없습니다.
변경된 것은 저장소입니다. 모든 수정 사항은 **추가 전용 로그(append-only log)**의 **작업(operation)**으로 기록되며, 이는 **노드별 샤드(per-node shards)**로 분할됩니다. 각 머신은 자신의 <node>.jsonl에만 추가합니다. 읽기 작업 시에는 디스크상의 모든 샤드를 그래프로 **폴딩(folds)**합니다.
이 폴드는 LWW-Element-Set입니다. 즉, 어떤 요소(엔티티, 관찰 발생 기록, 관계)가 존재하는지 여부는 해당 요소의 가장 큰 (HLC, node, sequence)를 가진 작업이 '추가'인지에 따라 결정됩니다. 이 타임스탬프는 하이브리드 논리 클록(hybrid logical clock)을 사용하므로 순서가 머신 간에도 인과적이고 안정적입니다. 삭제는 톰브스톤(tombstones)으로 처리하며, 관찰 기록을 삭제하는 것은 콘텐츠 레벨의 삭제이고, 엔티티를 삭제하면 해당 엔티티에 연결된 모든 관찰 기록과 관계도 톰브스톤 처리됩니다.
충돌할 수 없는 이유
이 폴딩 과정은 작업들의 **집합(set)**에만 의존하며, 그 순서나 그룹화에는 의존하지 않습니다:
- 멱등성(idempotent) — 동일한 작업을 두 번 적용해도 아무것도 변하지 않음;
- 교환법칙(commutative) — 샤드(shard)가 도착하는 순서에 상관없음;
- 결합법칙(associative) — 어떤 그룹으로 접든 같은 그래프를 얻음;
- 수렴성(convergent) — 동일한 작업을 본 두 노드는 동일한 그래프를 형성함.
따라서 잘못된 결과를 얻기 위한 병합 단계가 없습니다. 이 네 가지 법칙은 단순히 주장되는 것이 아니라 속성 테스트(property-tested)(fast-check)됩니다.
머신들이 수렴하는 두 가지 방법
1. 공유 영역(A shared area) — file 및 s3 백엔드입니다. 노드는 양쪽 모두가 볼 수 있는 것을 통해 샤드를 교환합니다: 이미 복제된 폴더(Syncthing, Dropbox, git 작업 트리, 공유 마운트) 또는 S3 호환 버킷을 사용합니다. 서버가 샤드를 **병합(merges)**하며, 이동시키지는 않습니다. 이 버킷은 조건부 쓰기(If-Match/If-None-Match)를 준수해야 합니다 — AWS S3, Cloudflare R2 및 MinIO가 이를 지원합니다.
2. 피어 투 피어(Peer to peer) — 동반 데몬인 nooniend는 공유 영역을 완전히 제거합니다. 이는 각 샤드를 사설 메시(Tailscale, WireGuard, LAN)를 통해 노드 간에 직접 복제합니다. 따라서 노드는 IP 도달 가능성만 필요합니다. 이 데몬은 작업을 생성하지 않습니다: 자신이 작성한 샤드를 푸시하고 보유한 레플리카를 제공하므로, 노드당 단일 작성자 규칙(per-node single-writer rule)이 여전히 유지됩니다. 멤버십은 가십(gossiped)되며, 활성 상태는 교환 결과에서 오고, 피어의 레플리카로부터 노드 자체의 손실된 샤드를 복원할 수 있습니다.
압축 (Compaction)
추가 전용 로그(append-only log)는 성장합니다. noonien compact는 각 요소별로 노드의 최신 작업을 유지하며 노드의 샤드를 다시 작성합니다 — 이 세트는 항상 같은 노드의 나중 작업에 의해 섀도잉되므로 병합 결과는 변하지 않습니다. 재작성은 원자적(atomic, 이름 변경 또는 S3에서의 조건부 쓰기)이며, 속성 테스트를 통해 압축이 병합된 그래프를 보존하는지 확인합니다.
이것이 아닌 것 (What it is not)
정직한 한계가 발표 내용보다 더 중요합니다:
- 설계상 최종 일관성 (Eventually consistent, by design). 노드는 다른 곳의 쓰기 작업을 즉시가 아닌 교환 후에 확인합니다.
- 노드 ID는 단일 작성자 샤드(single-writer shard)입니다. 정확히 하나의 서버 프로세스만이
<node>.jsonl에 데이터를 추가합니다. file/s3에는 공유 영역이 필요합니다.nooniend가 바로 이를 위해 존재합니다 (공유 영역 없이 수렴하는 것).
사용해 보기
npx -y server-noonien
server-noonien | 공식 server-memory | 호스팅 메모리 서비스 | |
|---|---|---|---|
| 다중 머신 | 예 — 노드별 샤드, 하나의 그래프 | 아니요 — 로컬 파일 하나 | 예, 서비스를 통해 제공 |
| ... |
저장소는 https://github.com/sequico/server-noonien에 있습니다. 피드백을 환영하며, 특히 operation-log와 compaction 설계, 그리고 peer-to-peer 동기화(sync)에 대한 의견을 부탁드립니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기