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

스냅샷은 영원해

~11 min · snapshots, append-only, content-addressed, preservation

Level 0불 꺼진 심지
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"엔진의 나머지 전부는 처음부터 재구축할 수 있어. 스냅샷은 못 해. 그래서 스냅샷은 다른 규칙 세트를 받아."

버릴 수 없는 유일한 저장소

이제 후렴은 익숙해: 인덱스는 파생, 버려, 재구축해. 근데 그 반사가 재앙이 될 저장소가 하나 있어 — 스냅샷 저장소, captured 코퍼스가 변환된 텍스트를 두는 곳. 소스 PDF 가 삭제됐는데 스냅샷까지 버렸으면, 그 내용은 우주에서 사라진 거야. 그래서 스냅샷은 인덱스와 반대 규율 아래 살아: 재구축되지 않고 보존돼.

Append-only 이고 content-addressed

스냅샷은 자기 내용의 해시로 저장돼. 문서의 변환된 텍스트가 어떤 sha256 으로 해시되고, 그게 id 이자 디스크상 주소가 돼. 결과 둘이 공짜로 떨어져: 동일한 내용은 한 번 저장되고(같은 해시, 같은 파일), 스냅샷은 절대 변형 안 돼. captured 소스가 바뀌면, 엔진은 옛 스냅샷을 덮어쓰지 않고 — 새 해시 아래 새 걸 append 해. 대체된 버전은 완벽히 읽히는 채 남아. 역사는 쌓이지, 제자리에서 편집되지 않아.

뇌관을 직접 뽑아야만 삭제돼

스냅샷은 귀하니까, 삭제가 부작용일 순 없어. 코퍼스를 제거해도 스냅샷은 안 지워져. 재색인해도 스냅샷은 안 지워져. 스냅샷이 죽는 유일한 길은 명시적이고 무장된 동작 — 정확한 해시를 두 번, 한 번은 확인으로 대는 삭제 호출 — 이고, 그마저도 살아 있는 문서가 아직 그것에서 파생되는 동안엔 conflict 에러로 거부돼. 스냅샷을 실수로, 대량으로, 또는 다른 동작의 결과로 삭제할 수 없어. 걸어가서 핀을 직접 뽑아야 해.

귀한 저장소엔 버릴 수 있는 저장소와 다른 규칙을 줘. 대부분 시스템의 실패는 모든 상태를 똑같이 다루는 거야 — 삭제 경로 하나, 복구 이야기 하나. 분리해: 파생 상태는 싸고, 버릴 수 있고, 마음대로 재구축돼. captured ground truth 는 append-only, content-addressed, 삭제엔 무장. 둘은 코드 경로를 공유하면 안 돼, 안 그러면 싼 쪽의 부주의가 결국 귀한 쪽에 닿아.

변환기도 다른 모든 것처럼 버전이 매겨져

마지막 불변성 하나: 스냅샷을 만든 변환기는 버전이 매겨져. strip 규칙이나 filter 임계값을 바꾸면 변환기 id 를 발행해 — 옛것의 동작을 절대 변형하지 마. 왜? 기존 모든 스냅샷은 특정 변환기가 만들었고, 그 변환기로 영원히 설명될 수 있어야 하니까. 옛 스냅샷 더하기 기록된 변환기 버전은, 그 텍스트가 어떻게 생겨났는지의 완전하고 재현 가능한 이야기야.

Code

Append-only 스냅샷, 무장된 단일 삭제 경로·python
from pathlib import Path
import hashlib

class SnapshotStore:
    root = Path("~/lantern/snapshots").expanduser()

    def append(self, text: str) -> str:
        sha = hashlib.sha256(text.encode("utf-8")).hexdigest()
        path = self.root / sha[:2] / f"{sha}.md"      # addressed by content
        if not path.exists():                          # identical text: store once
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(text)                       # write-once, never mutate
        return sha

    def delete(self, sha: str, confirm: str) -> None:
        if confirm != sha:
            raise ValueError("arm the delete: pass the sha as confirmation")
        if live_documents_deriving_from(sha):
            raise Conflict("snapshot still cited by a live document")  # 409
        (self.root / sha[:2] / f"{sha}.md").unlink()    # the ONLY deletion path

External links

Exercise

네 도구들이 뭘 어떻게 삭제하는지 봐. 한 가지를 삭제하거나 바꾸면 다른 게 조용히 삭제되거나 덮어써지는 곳 하나를 찾아(데이터를 데려가는 앱 삭제, 삭제를 미러링하는 동기화). 이제 그 cascade 가 절대 다시 못 만들 데이터에 닿는다고 상상해. 그걸 막았을 규칙을 설계해: 그 도구한테 '이 삭제를 명시적으로 무장해야 함' 은 어떻게 생겼을까?
Hint
패턴은: 귀한 저장소를 분리하고, 유일한 삭제 경로가 정확한 대상을 두 번 대게 만들고, 뭔가 아직 의존하는 동안엔 삭제를 거부해. 삭제가 부작용이면, 사고는 이미 예약된 거야.

Progress

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

댓글 0

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

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