C.W.K.
Stream
Lesson 05 of 05 · published

Writer 하나, Atomic 감사

~11 min · sqlite, wal, transactions, audit

Level 0Open Gate
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"변경과 변경의 기록은 함께 살거나 함께 죽어야 해. 하나가 다른 것 없이 일어날 수 있으면, 결국 딱 그 일이 생겨."

저장 레이어의 약속

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 고 — 가족 규모에선 진짜로 필요 없어.

필요 없는 concurrency 를 사지 마; 영원히 복잡성으로 지불돼. writer 여럿은 진짜 규모에선 진짜 요구야 — 그 아래에선 자해야. writer 하나는 분산 시스템 문제를 코드 직선으로 바꿔. concurrency 모델을 언젠가 필요할 거라 상상하는 아키텍처 다이어그램이 아니라 실제 부하에 맞춰.

atomic 감사 — 진짜 중요한 부분

이빨 달린 invariant 이 여기 있어: 모든 상태 mutation 은 변경 자체와 같은 트랜잭션에 before/after 감사 row 를 써. '변경, 그다음 곧 감사 로그' 가 아냐. 같은 트랜잭션. mutation 과 감사가 둘 다 commit 되거나, 둘 다 안 되거나. 이건 감사 흔적이 현실과 절대 어긋날 수 없다는 뜻이야: 값은 바뀌었는데 변경 기록이 없는 창도 없고, 안 바뀐 걸 바뀌었다고 감사가 말하는 창도 없어. 트랜잭션 경계로 용접돼 있어.

같은 트랜잭션이 아니면, 진짜 감사가 아냐. 쓰기 후의 로깅 호출은 감사처럼 보이지만 틈이 있어 — 프로세스가 그 사이에 죽어서, 기록 없는 변경이나 변경 없는 기록을 남길 수 있어. 믿을 만한 유일한 감사는 감사하는 대상과 함께 atomic 하게 commit 되거나 롤백되는 거야. 같은 트랜잭션이거나, 아니면 그냥 희망 섞인 로깅이야.
WAL 과 full synchronous 는 crash-safety 짝이야. WAL 은 쓰기 진행 중에도 reader 가 계속 읽게 하고 로그로 commit 을 durable 하게 해; full synchronous 는 데이터가 언제 디스크에 닿았는지 SQLite 가 거짓말 못 하게 해. 둘이 함께면 쓰기 중 정전이 일관된 DB 를 남겨 — 마지막 commit 된 상태, 반쯤 쓰인 row 는 절대 아냐.

Code

mutation 과 감사가 트랜잭션 하나를 공유 (예시)·python
def set_cash(owner, new_balance):
    with db.transaction() as tx:              # single writer, atomic 단위 하나
        before = tx.fetch_cash(owner)
        tx.update_cash(owner, new_balance)
        tx.insert_audit(                       # 변경과 SAME 트랜잭션
            table="cash", action="update",
            before=before, after=new_balance,
        )
    # commit 은 전부-아니면-전무: 새 잔고와 그 감사 row 가
    # 둘 다 착지하거나, 둘 다 안 해. 돈은 바뀌었는데 왜인지
    # 기록이 없는 중간 상태는 없어.

External links

Exercise

네가 지은 시스템에서 로그나 감사 항목도 쓰는 mutation 을 골라. 둘이 같은 트랜잭션이야, 아니면 감사가 쓰기 후의 별개 호출이야? 변경은 commit 됐는데 감사가 없게 — 또는 그 반대 — 만드는 정확한 실패(crash, exception, timeout)를 묘사해. 그다음 둘이 atomic 하게 함께 commit 되게 다시 써.
Hint
틈은 거의 항상 '쓰기 하고, 그다음 로그' 야. 그 두 줄 사이에서 프로세스를 죽이면 아무도 설명 못 하는 변경이나 롤백된 변경의 감사가 생겨. 둘을 트랜잭션 하나로 감싸면 틈이 물리적으로 존재 못 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.