"추가 전용은 정정이 자기가 무효화한 걸 제거한다는 뜻이다. 대체물 옆에 남겨두는 일은 절대 없다."
겉보기 모순
저장 불변식은 스냅샷이 추가 전용이라고 말해. 지표 역사 행은 계열, 날짜, 값, 출처, 가져온 시각을 들고 있다고. 그런 다음 같은 불변식이 정정은 삭제한다고 말해. 빨리 읽으면 두 문장이 싸워.
안 싸우는데, 화해시키는 데 필요한 사실이 셋이야. 그리고 불변식은 2026-08-31 까지 그중 둘만 적어놨어. 빠진 하나부터 보자. 이게 제일 놀랍거든.
빠져 있던 세 번째 사실
불변식은 이 행들을 "절대 고쳐 쓰지 않는다" 고 했어. 스키마는 다른 말을 하고. 기본 키가 data_date 로 끝나고 쓰기는 INSERT OR REPLACE 야. 같은 계열, 같은 날짜로 다시 가져오면 그 행을 제자리에서 덮어써. 3 주 넘게 그랬고, 문서는 반대로 적혀 있었어.
여기서 재밌는 건 코드가 맞다 는 거야. 일일 루프는 다시 돌릴 수 있어야 하거든. 재시도해도, 부분 실패 뒤에 다시 돌려도, 그날 두 번 돌려도 계열이 두 배가 되면 안 돼. 날짜별 유일 키에 덮어쓰기는 그걸 얻는 정직한 방법이고, 대안은 중복 행이랑 "진짜 어느 거야" 를 매번 읽기 시점에 푸는 거야. 더 나쁘지.
그러니까 진짜 성질은 날짜를 가로질러서는 추가 전용이고, 한 날짜 안에서는 멱등 이야. 이건 불변 로그가 아니라 멱등 upsert 고, 둘은 다른 걸 약속해. 제일 날카로운 결과가 이거야. 소스가 이미 발표한 값을 조용히 개정하면, 다음 패스가 옛날 값을 아무 흔적 없이 덮어써. 이 창고에 "이 날짜의 값이 예전엔 뭐였는지" 를 묻는 질문은 답이 없어. 아래 원칙 콜아웃이 그 경우를 역사라고 부르는데, 이 스키마에서는 저장이 안 돼. 불변식이 자기가 안 지키는 걸 약속하고 있었던 거야.
나머지 둘은 원래 불변식이 들고 있던 거고, 이 긴장을 푸는 게 이 레슨에서 제일 쓸모 있는 부분이야. 절반만 붙잡고 나머지 절반을 놓치는 팀이 꽤 많거든.
추가 전용이 실제로 지키는 것
추가 전용은 역사의 조용한 제자리 변형을 막으려고 존재해. 막으려는 실패는 예전엔 이거였는데 지금은 저거인 값이, 바뀌었다는 걸 어디에도 기록 안 한 채로 있는 상태야. 역사 계열을 못 믿게 만드는 건 그거야. 한 번 틀렸다는 게 아니라, 지금 보고 있는 게 기록된 그거인지를 더 이상 알 수 없다는 거.
그러니까 규칙은 이거야. 행의 값을 제자리에서 고치지 않는다. 새 값은 새 행으로 와. 역사는 수정되는 게 아니라 쌓여.
추가 전용이 아닌 것
추가 전용은 한 번 쓰인 행은 참이든 아니든 영원히 남아야 한다는 규칙이 아니야. 그렇게 읽으면 불변식이 거짓인 줄 아는 것들을 보존하는 기계가 돼. 그리고 계열별 최신 행을 집는 읽기 모델을 가진 저장소에서, 보존된 거짓은 무해한 역사 유물이 아니야. 제품이 내놓는 답이지.
그게 정확히 벌어진 일이고. 앞으로 찍힌 행들은 "기록용으로 남겨야 할 역사" 가 아니었어. 일어난 적 없는 장을 설명하고 있었고, 읽기 모델 맨 앞에 앉아 있었어. 그걸 갖고 있는다는 건 제품이 맞는 것보다 추상적 순수함을 고르는 거였을 거야.
실제 수리가 어떻게 생겼냐
이 사고의 수리는 이름 붙고 날짜 붙은 스크립트였어. DB 를 붙잡고 대화형으로 만진 게 아니라. 이 구분이 보이는 것보다 중요해.
스크립트는 임시방편 수정에 없는 성질 셋을 가져. 어떤 행을 무슨 기준으로 지우는지 코드로 정확히 말하고, 돌리기 전에 다른 사람이 읽을 수 있고, 끝난 뒤에도 뭘 했는지의 기록으로 저장소에 남아. 나중에 누가 "이 계열은 왜 8 월 초에 구멍이 있지?" 라고 물으면, 답이 버전 관리에 앉아 있는 이름과 날짜가 붙은 파일이야.