"변경과 변경의 기록은 함께 살거나 함께 죽어야 해. 하나가 다른 것 없이 일어날 수 있으면, 결국 딱 그 일이 생겨."
저장 레이어의 약속
snapshot 모델 밑엔 SQLite 가 앉아, 처리량보다 durability 로 설정돼: WAL(write-ahead logging), full synchronous 쓰기, foreign key 켜짐, busy timeout. 그리고 DB 위의 아키텍처 선택 하나: 정확히 하나의 application worker 가 모든 쓰기를 소유해. 같은 row 를 두고 경쟁하는 writer 풀이 아니라 — writer 하나, 그래서 누가 마지막에 썼는지 물을 일이 절대 없어. 가족 하나 local-first 앱엔 처리량이 제약인 적이 없었어; crash 하 정확성이 제약이었고, 이 설정 하나하나가 정확성을 사.
왜 single writer 가 모든 걸 단순하게 하나
SQLite 는 reader 여럿을 허용하지만 writer 를 직렬화해, Keep 은 거기 완전히 기대: 프로세스 하나, writer 하나. 이게 concurrency 버그 한 부류를 통째로 녹여. write-write race 도, 같은 row 를 read-then-write 하는 두 worker 의 lost update 도, 모든 테이블에 optimistic-locking 버전 컬럼도 필요 없어. busy timeout 이 드문 reader/writer 겹침을 우아하게 처리해. 없는 concurrency 는 방어 안 해도 되는 concurrency 고 — 가족 규모에선 진짜로 필요 없어.
atomic 감사 — 진짜 중요한 부분
이빨 달린 invariant 이 여기 있어: 모든 상태 mutation 은 변경 자체와 같은 트랜잭션에 before/after 감사 row 를 써. '변경, 그다음 곧 감사 로그' 가 아냐. 같은 트랜잭션. mutation 과 감사가 둘 다 commit 되거나, 둘 다 안 되거나. 이건 감사 흔적이 현실과 절대 어긋날 수 없다는 뜻이야: 값은 바뀌었는데 변경 기록이 없는 창도 없고, 안 바뀐 걸 바뀌었다고 감사가 말하는 창도 없어. 트랜잭션 경계로 용접돼 있어.