"변경과 변경의 기록은 같이 살거나 같이 죽어야 해. 하나만 따로 일어날 수 있는 구조라면, 언젠가 정확히 그 일이 벌어져."
저장 레이어가 하는 약속
snapshot 모델의 바닥에는 SQLite 가 있어. 처리량보다 durability 쪽으로 잔뜩 조여진 설정으로. WAL(write-ahead logging)에 full synchronous 쓰기, foreign key 켜짐, busy timeout 까지. 그리고 DB 위쪽에 아키텍처 결정이 하나 더 있어. 쓰기는 정확히 application worker 하나가 도맡아. 같은 row 를 두고 다투는 writer 풀 같은 건 없어. writer 가 하나니까 '마지막에 누가 썼지?' 라는 질문 자체가 생기지 않지. 가족 하나 쓰는 local-first 앱에서 병목은 처리량이었던 적이 없어. 진짜 제약은 crash 가 나도 정확할 것, 그거였고 이 설정 하나하나가 그 정확성을 사 오는 거야.
writer 하나가 모든 걸 단순하게 만든다
SQLite 는 reader 는 여럿 허용하고 writer 는 한 줄로 세워. Keep 는 거기에 아예 몸을 실었어. 프로세스 하나, writer 하나. 이 선택 하나로 concurrency 버그 한 부류가 통째로 녹아 없어져. write 끼리 부딪히는 race 도 없고, 같은 row 를 읽고-고치던 두 worker 가 서로의 업데이트를 지우는 일도 없고, 테이블마다 optimistic-locking 버전 컬럼을 달 필요도 없어. 드물게 reader 와 writer 가 겹치면 busy timeout 이 부드럽게 받아 줘. 없는 concurrency 는 방어할 필요도 없는 concurrency 야. 그리고 가족 규모에선 진짜로 없어도 돼.
atomic 감사, 여기가 진짜 핵심이야
물러설 수 없는 invariant 이 하나 있어. 모든 상태 mutation 은 변경 자체와 같은 트랜잭션 안에 before/after 감사 row 를 남긴다. '변경하고, 곧이어 감사 로그' 가 아니야. 같은 트랜잭션이야. mutation 과 감사가 둘 다 commit 되거나, 둘 다 안 되거나. 그래서 감사 흔적이 현실과 어긋날 수가 없어. 값은 바뀌었는데 기록이 없는 순간도, 바뀐 적 없는 걸 바뀌었다고 말하는 기록도 존재할 수 없지. 둘은 트랜잭션 경계로 용접돼 있으니까.