홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

AI 에이전트 메모리에 DB를 붙이게 된 이유 — Neon serverless Postgres 첫 파일럿

정기창·2026년 5월 10일

Claude Code를 본격적으로 자기 환경에 박아 쓰기 시작한 뒤로, 어느 순간부터 "메모리"라는 단어가 머릿속을 떠나지 않았습니다. 30개가 넘는 subagent, 60개가 넘는 스킬, 그리고 정해진 트리거에 따라 짧게 도는 자동화 스크립트들이 매일 무언가를 적어 두는데, 그 적어 둔 결과를 다시 읽고 합치는 일은 늘 사람이 손으로 했습니다. 파일은 점점 늘어나는데 SQL 한 줄로 "지난달 카테고리별 skip 비율" 같은 걸 뽑을 방법은 없었습니다.

그래서 처음으로 에이전트 메모리에 DB를 붙이기로 했습니다. 선택은 Neon serverless Postgres였고, 첫 파일럿은 한 자동화의 옵시디언/JSONL 적재 직후에 미러 INSERT 한 줄을 끼워 넣는 일이었습니다. 이 글은 그 PR이 무엇을 했는지, 그리고 그다음에 그리려는 큰 그림이 무엇인지에 대한 정리입니다.

왜 파일 기반 메모리만으로는 부족했는가

지금 제 환경의 메모리는 크게 두 갈래입니다. 하나는 ~/.claude/projects/-Users-jeongkichang-my-blog/memory/ 아래의 MEMORY.md와 50개가 넘는 피드백 룰·프로젝트 상태 노트입니다. 또 하나는 각 자동화가 옵시디언 노트와 JSONL 파일에 append하는 누적 기록입니다. 자동화 한 곳만 봐도 결정 이벤트가 376건, 누적 노트 100여 건, 메시지 백업 50여 건이 쌓여 있습니다.

사람이 읽기엔 잘 정리된 구조이지만, 한 가지가 결정적으로 부족했습니다. SQL 집계가 불가능하다는 것입니다. 월별 카테고리별 스킵 비율이 어떻게 변했는지, dogfood KPI 추세가 어떤지, UI/UX 평가 점수의 시간축 변화는 어떤지를 보고 싶을 때마다 매번 손으로 reduce하고 있었습니다. 한두 번은 괜찮은데, 같은 작업을 분기마다 반복하다 보니 이건 데이터 구조 문제라는 생각이 들었습니다.

결론은 단순했습니다. source of truth는 파일로 두되, DB를 미러로 추가하자. 파일은 사람이 읽고 git으로 추적하는 데 그대로 쓰고, DB는 SQL 집계와 외부 도구 활용을 위한 통로로만 둡니다. 양쪽이 어긋나면 언제나 파일이 정답이고, DB는 다시 백필하면 그만이라는 단방향 신뢰 모델로 시작하기로 했습니다.

왜 Neon serverless Postgres인가

DB 후보를 좁힐 때 기준은 분명했습니다. 개인 자동화에 쓸 거니까 idle 비용이 들면 안 되고, 데이터량은 1년 누적해도 수십 MB 수준이며, 스키마는 자주 바뀔 테니 실험용 분기가 필요했습니다. 그리고 스킬 호출이나 주기 트리거로 5분 안에 짧게 돌다 죽는 node *.mjs 프로세스가 주된 클라이언트라, TCP 커넥션 풀을 매번 새로 잡는 게 부담스러웠습니다.

Neon이 이 네 가지를 정확히 채워 주었습니다.

요구 조건 Neon이 제공하는 것
idle 비용 0 scales-to-zero (몇 분 idle 후 자동 sleep)
개인 데이터량 free tier 0.5 GB
스키마 실험 10 branches (main 운영, dev 실험)
short-lived 프로세스 HTTP driver @neondatabase/serverless

특히 마지막 항목이 결정적이었습니다. HTTP driver는 TCP 핸드셰이크와 풀 관리가 통째로 빠진 모델이라, 5초 안에 한두 번 INSERT 하고 끝나는 자동화에 정확히 맞는 모양이었습니다. 리전은 ap-southeast-1(Singapore)로 잡았고, 서울에서 50ms 내외 latency가 나왔습니다. 자동화는 어차피 비동기라 latency는 크게 신경 쓰지 않기로 했습니다.

도입 직후 글로벌 ~/.claude/CLAUDE.md에 connection string과 함께 몇 가지 사용 지침을 박아 두었습니다. 처음에는 임베딩 빼고 L1 구조 데이터만 적재(YAGNI), N+1 INSERT 금지(배치 또는 트랜잭션), 스키마 실험은 dev branch에서. 한 번 정해 두니 다음 영역을 추가할 때 의사결정을 다시 할 필요가 없어 편했습니다.

첫 파일럿 — PR로 무엇을 했나

여러 자동화 중에서 한 곳을 첫 케이스로 고른 이유는 세 가지였습니다. 누적 데이터가 가장 활발하고, 옵시디언과 JSONL에 이중으로 쌓는 구조라 전환을 검증하기 깔끔하고, 이미 기존 stats 명령이 옵시디언 파일을 reduce해서 도는 코드라 SQL 한 줄로 대체했을 때 효과가 즉시 체감되는 영역이었습니다.

전체 전환은 3단계로 나누었습니다.

Phase 1 (선행 완료): Neon 프로젝트의 dev + production branch 양쪽에 5 테이블 생성
  ├─ leads          (lead 마스터, lead_id UNIQUE)
  ├─ events         (결정 이벤트, event_id UNIQUE)
  ├─ quotes         (lead 본문, lead_id FK)
  ├─ chats          (채팅방 메타, chat_id UNIQUE)
  └─ chat_messages  (메시지, UNIQUE (chat_id, dedupe_key))

Phase 2 (이번 PR): automation dual-write hook
  옵시디언/JSONL append 직후 Neon INSERT
  옵시디언/JSONL = source of truth, Neon = 미러

Phase 3 (대기): 옵시디언 1주 분량 백필 일회성 스크립트
  Phase 2를 1주 돌려 dual-write 검증한 뒤 진행

이 PR(+1550 / -2, 11 파일)에서 가장 중요한 변경은 두 가지였습니다. core/neon-client.mjs를 신설해 5개 export 함수와 ping을 두었고, 이벤트 적재 모듈의 appendEvents() 직후에 insertEvents() 호출을 한 줄 끼워 넣었습니다. 메시지 sync 모듈에는 neonSync()라는 헬퍼를 두고 4개 return 지점 직전에서 호출하도록 했습니다.

핵심 패턴 — 옵시디언/JSONL 다음 줄에 미러 INSERT

가장 먼저 정한 원칙은 자동화의 메인 흐름이 절대 Neon 때문에 끊기지 않게 한다는 것이었습니다. 옵시디언/JSONL 쓰기가 성공한 시점에서 데이터 손실은 0이고, Neon이 죽든 네트워크가 끊기든 그 다음 줄에서 일어나는 일은 모두 미러 쪽 사정입니다.

// 이벤트 적재 모듈의 main() 끝쪽
appendEvents(normalized);                             // 1) JSONL append (source of truth)

try {
  const neonClientPath = join(__dirname, '..', 'core', 'neon-client.mjs');
  const { insertEvents } = await import(neonClientPath);
  const r = await insertEvents(normalized);           // 2) Neon dual-write (mirror)
  if (VERBOSE) console.log(`[neon] inserted ${r.inserted}/${normalized.length} events`);
} catch (e) {
  console.error(`[neon] insertEvents failed (non-fatal): ${e.message}`);  // 3) 실패해도 흐름 유지
}

이 패턴이 안전하게 굴러가려면 미러 쪽이 세 가지 성질을 갖춰야 합니다.

  • Idempotent INSERT — 모든 export 함수가 ON CONFLICT 기반이라, 한 번 실패한 INSERT가 있어도 다음 실행에서 자연스럽게 재시도되어 복구됩니다. 옵시디언이 정답이고 DB는 따라잡으면 된다는 단순한 모델이 운영에서 가장 편안했습니다.
  • Lazy 초기화 — Neon 클라이언트는 모듈 top-level이 아니라 getSql()의 첫 호출 시점에만 환경변수를 검증하고 캐시합니다. 모듈 import 시점에 throw하면 caller의 try/catch 바깥이라 메인 흐름이 그대로 죽고, 동적 import의 경우 매번 같은 에러를 다시 잡아 stderr 로그가 폭증합니다. non-fatal 경계는 caller의 try/catch 안쪽에서만 성립한다는 점을 한 번 더 새겼습니다.
  • 충돌 안전한 dedupe key — 메시지 dedupe key를 sent_at|sender|body첫30자로 잡으면 동일 시각 동일 sender의 빈 body 메시지(예: 시스템 알림) 두 건이 들어왔을 때 두 번째가 ON CONFLICT DO NOTHING에 걸려 silent drop됩니다. 외부 API가 server-side message id를 주면 그걸 1차로 쓰고, 없으면 빈 body 시 raw.type을 섞어 충돌을 피하도록 두 단계로 나눴습니다.

HTTP driver를 처음 쓰면서 한 가지 더 배운 게 있습니다. SQL 한 번이 그대로 HTTP 요청 한 번이라, 같은 batch 안에 같은 lead_id의 stub upsert가 N번 들어가면 곧장 N번의 round-trip 비용으로 보입니다. Map으로 한 번 dedupe해서 stub은 unique lead 수만큼만 날리도록 했습니다. 익숙한 N+1의 변형인데, HTTP driver는 latency를 직접 보여 줘서 더 빨리 깨닫게 됩니다.

이 다음에 그릴 큰 그림

Phase 2가 1주 정도 안정적으로 돌면, 다음은 두 갈래로 갈라집니다. 한쪽은 옵시디언에 이미 쌓여 있던 과거 데이터를 Neon으로 백필하는 일이고, 다른 한쪽은 이 패턴을 다른 영역으로 옮겨 가는 일입니다.

Phase 3 — 옵시디언 1주 분량 백필

먼저 1주 동안 옵시디언/JSONL 쪽과 Neon 쪽이 얼마나 어긋나는지를 본격적으로 측정합니다. JSONL의 라인 수와 SELECT COUNT(*) 결과가 일치하는지, chat 메시지의 dedupe key 분포가 의도대로 들어갔는지, 0건 차이가 나는 케이스가 있는지를 봅니다.

측정이 끝나면 옵시디언 vault에 이미 쌓여 있던 1년치 노트를 일회성 스크립트로 백필합니다. ON CONFLICT가 모든 INSERT의 안전망이라, 이미 dual-write로 들어간 최근 1주 데이터를 다시 INSERT해도 문제가 없습니다. 백필 스크립트가 idempotent라는 사실이 작업의 부담을 크게 덜어 줍니다.

적용 영역을 어디까지 늘릴 것인가

한 자동화에서 패턴이 검증되면, 같은 흐름을 다른 영역으로 옮기는 게 다음 단계입니다. 지금 후보로 보고 있는 영역은 다섯 갈래입니다.

  • dogfood KPI — 페르소나×스텝별 P0~P3 발생 횟수를 분기마다 비교 가능한 형태로 누적
  • UI/UX 평가 trend — 페이지별 audit/critique 점수의 시간축 변화
  • audit findings — 보안/품질/아키텍처 영역별 발견 항목과 P0~P3 분포
  • research log — 같은 주제를 재조사할 때 과거 결론을 바로 가져올 수 있는 시계열 레퍼런스
  • 자동화 결정 이벤트 전반 — 첫 파일럿과 같은 패턴을 그대로 적용해 다른 자동화들도 SQL 집계권에 합류

각 영역의 데이터 모양은 다르지만, 적용 절차는 같은 3단계입니다. 스키마를 정의하고, source of truth append 직후에 dual-write hook을 박고, 1주 검증 뒤 일회성 백필. 한 번 정해 둔 패턴이 다음 영역의 의사결정 비용을 크게 깎아 줍니다.

이 누적이 열어 주는 질문들

여러 영역이 같은 DB로 모이고 나면, 지금까지 묻기 어려웠던 질문들이 SQL 한 줄로 답이 됩니다.

  • "지난 분기 dogfood에서 P0가 가장 많이 나온 페르소나×스텝 조합은?"
  • "audit findings의 영역별 P0 추세가 최근 6주간 어떻게 변했나?"
  • "research log에서 'Neon' 또는 'PostgreSQL'을 다룬 과거 조사 결과를 시간순으로"
  • "여러 자동화가 만든 결정 이벤트 중 같은 트리거 키워드를 공유한 케이스는?"

지금까지는 사람이 옵시디언을 grep하고 Dataview로 reduce해 가며 어렵게 만들던 답이, SQL 한 줄로 끝나는 영역이 점점 넓어집니다. 그게 이번 PR이 첫 단추인 큰 그림입니다.

마무리

돌이켜 생각해 보면, "AI 에이전트의 메모리"라는 말이 처음에는 거창하게 들렸는데 실제로는 평범한 데이터 모델링 문제였습니다. source of truth는 어디인가, 미러는 어디인가, dedupe key는 무엇인가, 실패는 어디까지 fatal인가. Claude Code와 자동화가 매일 적어 두는 기록을 SQL로 한 번 쓸어 담을 수 있게 되니, 다음에 어떤 질문을 던져도 답을 만들 수 있겠다는 자신감이 조금 생겼습니다. 그 첫 시작을 기록해 둡니다.

Claude CodeAI 에이전트NeonPostgresServerless자동화dual-write

관련 글

Claude Code 라우팅 매트릭스 빈 행 3개를 새 전문 에이전트로 메운 회고

Claude Code 라우팅 매트릭스를 글로벌 CLAUDE.md 와 review-loop SKILL.md 에 박은 직후, 24행을 다시 들여다보니 fallback 으로 흘러갈 수밖에 없던 도메인 세 개가 어렵지 않게 떠올랐습니다. devops-engineer · dba · test-data-verifier 세 전문 에이전트를 더하며 만난 정의 작업의 무게를 정리한 후속편입니다.

관련도 89%

Claude Code 사용 패턴, transcript jsonl 로 직접 진단해봤습니다

스킬·서브에이전트를 잔뜩 만들어놓고도 정작 일하고 있는지 확신이 없어, Claude Code 가 자동으로 쌓는 transcript jsonl 을 jq 와 bash 로 들여다봤습니다. general-purpose 비중·dormant 스킬까지 데이터로 본 자가 진단 회고입니다.

관련도 89%

대화 세션에서 돌리던 slash skill을 배치 자동화로 옮긴 이야기

매일 같은 slash skill을 손으로 돌리던 습관을 LaunchAgent 배치로 옮기면서 느낀 것은 기술 장벽보다 역할 재정의가 본질이라는 점이었습니다. 집행자에서 큐레이터로의 전환에 대한 기록입니다.

관련도 88%