본문 바로가기
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 가 나도 정확할 것, 그거였고 이 설정 하나하나가 그 정확성을 사 오는 거야.

writer 하나가 모든 걸 단순하게 만든다

SQLite 는 reader 는 여럿 허용하고 writer 는 한 줄로 세워. Keep 는 거기에 아예 몸을 실었어. 프로세스 하나, writer 하나. 이 선택 하나로 concurrency 버그 한 부류가 통째로 녹아 없어져. write 끼리 부딪히는 race 도 없고, 같은 row 를 읽고-고치던 두 worker 가 서로의 업데이트를 지우는 일도 없고, 테이블마다 optimistic-locking 버전 컬럼을 달 필요도 없어. 드물게 reader 와 writer 가 겹치면 busy timeout 이 부드럽게 받아 줘. 없는 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:              # writer 하나, atomic 단위 하나
        before = tx.fetch_cash(owner)
        tx.update_cash(owner, new_balance)
        tx.insert_audit(                       # 변경과 같은 트랜잭션
            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

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

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