DB가 멀쩡하다고 배포하지 마라: p-layer v0.9.0에서 배운 메모리 설계 순서
DB가 멀쩡하다는 말은 어디까지인가
메모리 시스템을 만들 때 가장 위험한 순간은 “스키마는 정상입니다”라고 말하고 다음 작업으로 넘어갈 때다. 테이블이 열리고, 데이터가 보이고, 테스트 몇 개가 통과하면 안심한다. 그런데 다음 세션에 에이전트가 낡은 규칙을 불러오거나, 마이그레이션이 조용히 빠지거나, 운영 환경에서만 확장 모듈이 깨진다. 이번에 p-layer v0.9.0을 read-only로 다시 훑으며 내가 확인한 결론은 단순했다. 구조적 건강성과 실행 검증은 서로 다른 판정이다.
이 글의 근거는 외부 기사나 트렌드 통계가 아니다. 내가 에이전트와 함께 지정 커밋의 소스, 테스트, 로컬 DB 스키마를 읽고 실행한 상호작용(에이전트-드루 대화·작업)이다. 그래서 “좋은 설계”라고 포장하기보다, 어디까지 확인했고 어디서 멈췄는지를 같이 기록한다.
첫 번째 실수: DB를 하나의 상태로 본 것
이번 감사 대상은 ab22071, 태그로는 v0.9.0인 p-layer 저장소였다. 소스는 꽤 정직했다. SQLite 저장소에는 schema_migrations, knowledge, FTS 인덱스, embeddings, episodes, 그래프 테이블, 규칙, 스냅샷, 감사 로그가 각각 존재한다. 마이그레이션은 현재 v4까지 올라가고, 이미 적용된 SQL의 checksum이 달라지면 다음 버전을 추가하라고 실패한다.
반면 로컬의 .knowledge/knowledge.db는 다른 모양이었다. 읽기 전용 연결에서 PRAGMA quick_check는 ok였지만, 테이블은 p-layer의 knowledge가 아니라 기존 memory, entities, relations와 FTS 테이블이었다. 이 DB를 보고 “새 스키마가 이미 적용됐다”고 해석하면 바로 실수다. 무결성은 통과했지만, 제품 스키마와의 동일성은 통과하지 않았다.
파일이 열리는가, 데이터가 깨지지 않았는가, 기대한 스키마인가, 현재 코드가 그 스키마를 관리하는가. 이 네 질문을 분리해야 한다.
구조가 건강하다고 판단한 근거
p-layer의 좋은 점은 “정상”을 한 번의 성공 메시지로 처리하지 않는다는 데 있다. 내가 확인한 구조적 방어선은 네 개였다.
- Forward-only migration: 스키마 변경을 기존 SQL 수정으로 덮지 않고 새 버전으로 추가한다.
- Checksum: 이미 적용된 migration의 SQL이 바뀌면 조용히 진행하지 않고 오류를 낸다.
- 읽기 전용 doctor: DB를 만들거나 migrate하지 않고, 필수 테이블·migration·quick check·journal·FTS 개수를 따로 검사한다.
- 쓰기 경계의 거버넌스: P0-P6 레이어 ACL을 코드에서 검사하고, 허용되지 않은 쓰기도 audit log에 남긴다.
특히 migration checksum은 작은 장치지만 효과가 크다. 예전 방식처럼 모든 곳에서 CREATE TABLE IF NOT EXISTS를 반복하면 현재 DB가 어떤 버전인지 알기 어렵고, 이미 있는 테이블에 컬럼만 덧붙이는 코드가 여러 군데 생긴다. 그러면 “동작하는 것처럼 보이는 두 개의 스키마”가 생긴다. p-layer는 그 모호함을 버전과 checksum이라는 파일럿 램프로 밝혀둔다.
if version in applied:
if stored_checksum != checksum(current_sql):
raise RuntimeError("add a new migration instead")
continue
또 하나의 선택도 중요했다. 신규 Store 연결은 WAL과 foreign key를 켠다. 그러나 legacy DB를 그대로 관리하는 drewdb 경로에서는 foreign key를 무조건 켜지 않는다. 과거 데이터가 제약 조건을 만족한다는 보장이 없기 때문이다. 대신 연결을 additive하게 만들고, 관리 버전과 schema checksum을 별도 p_layer_meta에 기록한다. 기존 데이터를 건드리지 않으면서 관리 주체만 분명히 하는 방식이다.
두 번째 실수: 테스트 통과를 구조 건강성으로 착각한 것
지정 커밋에서 전체 테스트를 실행했다. 결과는 167개 실행, 23개 skip, 1개 failure, 1개 error였다. README의 “167 passed” 배지와 내 로컬 실행 결과를 섞어 쓰지 않기로 했다. PostgreSQL 서버가 없는 로컬에서는 관련 parity 테스트가 skip됐고, macOS에서는 MCP SDK 테스트도 환경 사유로 skip됐다. 이 숫자는 실패한 제품이라는 뜻도, 완전히 검증됐다는 뜻도 아니다. 무엇을 실제로 돌렸는지 보여주는 실행 증거다.
실패 두 개도 분해해 보니 스키마 파손은 아니었다. 하나는 없는 vec0 확장 라이브러리를 우아하게 건너뛰려던 경로에서 현재 Python의 SQLite 연결 객체가 enable_load_extension을 제공하지 않아 발생했다. 다른 하나는 recency 테스트가 고정한 날짜가 현재 실행 시점 기준 30일 창 밖으로 밀려나 boost가 0이 된 경우였다. 전자는 플랫폼 capability 확인이 빠진 문제이고, 후자는 시간에 기대는 fixture가 낡은 문제다.
여기서 배운 규칙은 명확하다. schema health, compatibility, behavior, environment를 한 테스트 묶음의 초록불로 합치지 말 것. 각각 다른 질문과 다른 실패 메시지를 가져야 한다. 그래야 스키마가 정상인데도 기능 테스트가 깨졌을 때, DB를 괜히 마이그레이션하거나 데이터를 복구하는 사고를 피할 수 있다.
내 프로젝트에 옮긴다면, 이 순서로 만든다
내가 다음 메모리 저장소를 만든다면 처음부터 검색 품질보다 상태 판정 순서를 고정한다. 최소 구현은 아래 다섯 단계면 충분하다.
- Inventory: read-only로 실제 테이블, 인덱스, journal mode, foreign key 위반, row count를 확인한다.
- Version: schema version과 migration 이름을 저장하고, 적용된 DDL checksum을 검증한다.
- Boundary: legacy DB를 신규 Store와 같은 것으로 취급하지 않는다. importer나 compatibility adapter를 경계로 둔다.
- Doctor: 진단 명령은 DB를 생성하거나 변경하지 않게 만든다. “pending migration”과 “integrity failure”를 다른 상태로 출력한다.
- Matrix: SQLite/PostgreSQL, 운영체제, 확장 모듈 유무, 고정 시간 fixture를 분리해 테스트한다.
이 순서의 핵심은 1번과 3번이다. 대부분의 사고는 schema.sql을 작성하는 순간이 아니라, 이미 존재하는 DB를 새 코드가 자기 것이라고 가정하는 순간 생긴다. 먼저 읽고, 차이를 이름 붙이고, 그 다음에 이식한다. p-layer의 import_drewgent가 소스 컬럼을 실제로 확인해 entities 구조 차이를 적응시키는 이유도 같다. 호환성은 “다 지원한다”는 문장이 아니라, 차이를 검사하는 코드다.
결론: 건강한 구조는 실패를 숨기지 않는다
이번 audit에서 내가 확인한 p-layer의 구조적 장점은 모든 테스트가 초록이었다는 데 있지 않다. 실제로 그렇지 않았다. 장점은 더 낮은 단계에 있다. migration이 버전을 알고, checksum이 드리프트를 막고, doctor가 읽기 전용으로 상태를 설명하고, write ACL과 audit log가 실패를 기록한다. 그래서 실행 실패가 생겼을 때 “DB가 망가졌다”는 막연한 결론 대신, 확장 로딩인지 시간 fixture인지 스키마인지 좁혀갈 수 있었다.
당신의 프로젝트에도 먼저 검색 기능을 붙이지 말고 이 네 가지를 붙여보길 권한다. schema_version, checksum, doctor --read-only, denied write audit. 메모리가 똑똑해지는 속도보다, 잘못된 상태를 정직하게 드러내는 속도가 장기적으로 더 중요하다.
지금 운영 중인 DB에 read-only 점검을 한 번 실행해보자. 먼저 quick check와 실제 테이블 목록을 저장하고, 현재 코드가 기대하는 migration 목록과 비교하라. 결과를 댓글로 공유해도 좋다. “정상”이라는 한 단어를 네 개의 판정으로 쪼개는 순간, 다음 마이그레이션에서 같은 실수를 반복할 가능성이 줄어든다.
근거 출처
외부 정보(하베스터 수집): 사용하지 않았다. 기사·트렌드·외부 통계로 주장을 보강하지 않았다.
상호작용(에이전트-드루 대화·작업): /Users/drew/workspaces/p-layer의 Git 커밋 ab22071(v0.9.0) 소스와 테스트를 read-only로 검토했다. p_layer/migrations.py, p_layer/store.py, p_layer/health.py, p_layer/drewdb.py, p_layer/import_drewgent.py 및 관련 테스트를 읽었고, 해당 커밋에서 python3 -m unittest discover -s tests -v를 실행했다. 로컬 .knowledge/knowledge.db는 쓰기 없이 quick_check, journal mode, 테이블·컬럼 구조만 확인했다.