본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

저장한 건 영원하고, 저장 안 한 건 퇴비야

~11 min · retention, storage, invariants, product-boundary

Level 0미분류
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

리더는 생각보다 빨리 쌓여

출처 마흔 개를 삼십 분 주기로 돌리면 하루에 수천 행이야. 그중 거의 아무것도 안 읽혀. 도착해서, 최근 창에 몇 시간 앉아 있다가, 똑같은 것들한테 밀려나. 그걸 다 영원히 들고 있는 건 원칙이 아니라 관성이야. 아무도 다시 안 볼 재료 때문에 모든 질의랑 백업이랑 인덱스가 부풀거든.

그래서 리더한테 보존 정책이 필요해. 그리고 그걸 쓰는 순간 그 예외에 대해 약속을 한 거야. 저장 버튼이 있는 이유 전체가 저장이 뭔가를 의미한다는 거니까.

두 부류를, 소리 내어

실사용을 견디는 정책은 무뚝뚝해. 기간 지난 저장 안 한 기사는 퇴비, 저장한 기사는 영구, 그리고 모든 이벤트는 가리키는 기사가 어떻게 됐든 영구. 뉴스 리더한텐 두 주가 합리적인 창이야. 읽다 만 게 사라지지 않을 만큼 길고, 저장고가 작업 집합으로 남을 만큼 짧고.

나중에 공유도 저장이랑 같이 영속성 주장에 합류했어. 그 논리는 기술이 아니라 제품 논증이라 남겨둘 값어치가 있고. 기사를 발행하는 건 그냥 챙겨두는 것보다 강한 약속이고, 자기가 올린 글 선반이 두 주 뒤에 조용히 비는 건 애초에 없던 것보다 나쁜 거짓말이니까.

삭제자는 하나, 이름 붙여서

여기가 구조적인 부분이야. 보존 함수가 어느 행을 봐줄지 조심하는 걸로는 부족해. 시스템 전체에서 기사를 지우는 코드 경로가 정확히 하나여야 하고, 나머지 모든 경로는 구조적으로 그럴 수 없어야 해. 어떤 캐스케이드도 기사 표에 닿으면 안 되고, 어떤 정리 작업도 지름길을 타면 안 되고, "테스트용으로만" 있는 관리 도우미도 있으면 안 된다는 뜻이야.

이유는 영속성 보장이 그 주제를 자기가 소유한다고 생각하는 함수 하나가 아니라 가능한 모든 실행에 대한 주장이라서야. 조심스러운 삭제자 하나 더하기 부주의한 캐스케이드 하나는 보장이 아예 없는 시스템이고, 다음 강의가 정확히 그 얘기고.

보존은 유도된 것 전부를 덮어

잊기 쉬운 마지막 조각. 쌓이는 게 기사만이 아니야. 수집 로그, 캐시된 샘플, 렌더링된 오디오, 추출된 본문 — 유도된 산물마다 창이 필요하고, 자기가 설명하는 것의 수명에 묶여야 해. 일 년 전에 퇴비가 된 기사를 설명하려고 영원히 보관되는 수집 로그는 순수한 침전물이야.

보존 정책은 약속이고, 약속엔 집행자가 정확히 하나 필요해. 보호 대상을 지울 수 있는 코드 경로가 둘이면 정책이 아니라 관행이 있는 거고, 관행은 이 코드를 안 읽고 다음 기능을 쓰는 사람이 깨.

Code

기사 삭제자 하나, 그리고 유도된 것 전부를 위한 퇴비 패스·python
def prune(con) -> int:
    """The ONLY thing in this system that deletes an article.

    Everything else that wants an article gone must go through here.
    No cascade reaches this table (see the next lesson for why that
    sentence had to be earned), and events survive in the JSONL log
    regardless of what happens to the row they point at.
    """
    cutoff = (datetime.now(timezone.utc)
              - timedelta(days=RETENTION_DAYS)).isoformat(timespec="seconds")
    cur = con.execute(
        """DELETE FROM articles WHERE fetched_at < ?
           AND id NOT IN (
               SELECT article_id FROM article_state
               WHERE saved_at IS NOT NULL OR shared_at IS NOT NULL
           )""",
        (cutoff,),
    )
    con.commit()
    return cur.rowcount


def compost(con) -> dict:
    """Retention covers everything DERIVED, not only articles. Each
    derived artifact ages out against the lifetime of what it explains."""
    return {
        "articles":      prune(con),
        "fetch_log":     prune_fetch_log(con),      # diagnostic, not truth
        "trend_samples": prune_trend_samples(con),  # per-topic depth
        "tts_cache":     prune_tts_cache(con),      # keyed to live articles
    }

External links

Exercise

네가 관리하는 시스템에서 무한정 자라는 데이터를 찾아서, 원하는 보존 규칙을 예외까지 포함해 한 문장으로 써봐. 그러고 나서 지금 그 보호 대상을 지울 수 있는 코드 경로가 몇 개인지 세봐. 캐스케이드, 정리 작업, 실환경에 대고 도는 테스트 픽스처까지 확인하는 거 잊지 말고. 하나가 아니면 정책이 아니라 관행이 있는 거야.
Hint
사람들이 놓치는 건 캐스케이드야. 스키마에 한 번 선언되고 나면 삭제를 찾아 grep 할 어떤 코드에도 안 나타나거든. 개수를 믿기 전에 보호 대상 표를 가리키는 외래 키를 나열하고 ON DELETE 절을 읽어봐.

Progress

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

댓글 0

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

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