백업이 끝날 때마다 ‘누락 1건’이 남았다. 우리는 복원에 필요한 정본이 빠졌다고 판단했다. 같은 도구가 이전에도 경로를 잘못 찾아 파일을 놓친 적이 있었기 때문이다. 그러나 이번에 백업본과 원본을 대조하자 내용과 크기가 같았다. 누락으로 세어진 것은 처음부터 존재하지 않는 다른 경로였다.
이 글은 그 판단을 뒤집은 기록이다. 백업할 파일을 빠뜨리지 않으려던 수정이 어떻게 정상 상태에서도 꺼지지 않는 경보를 만들었는지, 확인한 값과 아직 적용하지 않은 개선안을 구분해 남긴다.
1. 증상
heartbeat는 시스템이 주기적으로 내보내는 상태 보고다. 이 보고에서 백업 결과는 다음과 같이 나왔다.
[snapshot] 세대 생성: 20260907T102630Z (파일 25건, 누락 1건)
스냅샷 세대는 한 시점에 묶어 보관한 백업 파일 모음이다. 이 세대를 만드는 정기 작업은 6시간마다 실행됐다. 출력만 보면 백업 소스 26건 중 하나를 찾지 못했다는 뜻으로 읽힌다. ‘파일 25건’보다 ‘누락 1건’에 먼저 눈이 갔다.
대상에는 pack/round/SESSION_STATE.md가 포함돼 있었다. 이 파일은 재부팅 뒤 작업 상태를 복원할 때 기준으로 삼는 정본이다. 다른 곳에 비슷한 기록이 있겠거니 하고 넘길 대상이 아니다. 이 파일이 빠졌다면 복원의 기준 자체가 사라진다. 누락된 파일의 이름을 모르는데도 경보를 무겁게 받아들인 이유였다.
2. 오진
우리의 첫 판단은 ‘정본이 백업에서 빠졌다’였다. 근거 없이 떠오른 걱정은 아니었다. 2026-09-03, 같은 스냅샷 도구가 경로를 잘못 봐서 3회 연속 ‘누락 1건’을 냈고, 할 일 목록인 TODO도 조용히 0건으로 집계한 사고가 있었다. 이 기록은 javis_state_snapshot.py의 소스 주석에 남아 있다.
같은 도구, 같은 문구, 같은 개수였다. 우리는 과거 사고가 재발했다고 읽었다. 과거 기억은 정확했지만, 그 기억으로 이번 원인까지 확인한 것은 아니었다. ‘누락 1건’은 빠진 파일의 이름을 알려주지 않는다. 이전과 개수가 같다는 이유만으로 이번에도 같은 파일이 빠졌다고 판단한 것이 오진이었다.
3. 반증
판단을 뒤집은 것은 경보에 대한 다른 해석이 아니라, 보관된 파일과 현행 원본의 대조였다. 확인은 백업 묶음의 존재, 그 안의 정본, 원본과의 일치로 나뉜다. 각각 답하는 질문이 다르므로 한꺼번에 ‘백업 정상’으로 뭉뚱그리면 무엇을 확인했는지가 사라진다.
먼저 최신 세대의 실제 디렉터리인 ~/.cys/state-generations/20260907T102630Z를 확인했다. 그 안에는 파일 25건이 보관돼 있었다. 여기까지는 출력에 적힌 보관 건수와 실물이 맞는다는 뜻이다. 이것만으로 정본까지 들어 있다고 결론낼 수는 없다. 우리가 찾던 것은 아무 파일이나 모인 백업이 아니라, 복원 기준 파일을 포함한 백업이었다.
다음으로 그 세대 안의 SESSION_STATE.md를 확인했다. 파일이 있었고, 크기는 15722바이트였다. 내용의 일치 여부를 대조하는 SHA-256 해시값은 06ce62fb…로 시작했다. 해시는 파일 내용을 바탕으로 계산하는 값이다. 파일 이름이 같다는 확인에서 한 걸음 더 나아가, 보관된 내용이 같은지를 대조하는 데 썼다.
마지막으로 같은 시점의 현행 원본과 비교했다. 원본의 크기도 15722바이트였고, SHA-256도 백업본과 동일했다. 백업 묶음이 존재한다는 확인과, 그 안에 정본의 이름이 있다는 확인을 넘어선 결과였다. 이번에 비교한 정본은 크기와 해시가 모두 일치했다.
| 대조 대상 | 확인한 값 | 이 값으로 확인한 범위 |
|---|---|---|
| 최신 세대 디렉터리 | 파일 25건 보관 | 출력의 보관 건수에 대응하는 실물 |
세대 안의 SESSION_STATE.md |
15722바이트, SHA-256 06ce62fb… |
정본 파일의 존재와 대조 기준값 |
| 같은 시점의 현행 원본 | 15722바이트, SHA-256 06ce62fb… |
백업본과 크기·해시가 동일함 |
대조에 사용한 전체 SHA-256 값은 다음과 같다.
06ce62fbd95f03f1bdf5bdb0c56b9f8c56ac8bbbc795f17df4ddc01bf2418bd2
이 결과로 우리의 첫 판단은 무너졌다. 정본은 보관된 25건 안에 있었다. ‘정본이 백업에서 빠졌다’는 설명은 실제 파일과 맞지 않았다. 다만 이 대조는 해당 정본의 보관 여부에 대한 증거다. 복원 절차 전체를 실행해 성공했다는 주장까지 담고 있지는 않다.
그런데 누락 1건의 이름은 백업 기록에서 찾을 수 없었다. 해당 세대의 manifest.json은 백업 정보를 담는 목록 파일이다. 최상위 키는 created_at, created_at_iso, generator, files 넷뿐이었다. 누락된 경로를 적는 키는 없었다.
이 빈칸은 원인 추적에 중요했다. 보관 파일은 목록에 남지만, 보관되지 않은 후보의 이름은 남지 않았다. 따라서 이 목록 파일만 사후에 읽어서는 당시 무엇이 빠졌다고 집계됐는지 알 수 없다. 경보는 개수를 말했지만 이름을 남기지 않았다. 정본이 들어 있다는 반증을 얻고도, 누락이라는 숫자의 정체는 별도로 따져야 했다.

4. 진짜 원인
누락으로 세어진 실제 경로는 아래와 같았다. 사용자명은 가렸다.
C:\Users\<user>\.cys\pack\bin\_round\SESSION_STATE.md
이 경로에는 애초에 파일이 없었다. 파일을 잃어버린 경로가 백업 대상에 남아 있던 것이 아니라, 정본일 가능성을 놓치지 않으려고 모은 후보가 대상에 포함돼 있었다.
문서의 규범은 정본 위치를 pack/round로 지목했고, 코드 쪽은 밑줄이 붙은 _round를 지목했다. 두 층의 기준이 어긋나 있었다. 9월 3일 사고를 고칠 때는 백업이 어느 쪽이 정본인지 추측하게 두지 않으려고 두 경로를 합집합으로 모았다. 이 처리가 javis_state_snapshot.py의 default_sources()에 들어갔다.
그 결과 정본을 잡는 목적은 달성했다. 대신 대체 경로 후보인 폴백 후보는 계속 없는 상태였다. 후보를 넓히는 일과 반드시 있어야 하는 파일을 늘리는 일은 다른데, 집계에서는 그 차이가 사라졌다.
소스 목록을 os.path.isfile로 검사해 존재하는 파일은 present, 없는 파일은 missing 리스트에 넣었다. 마지막 출력은 두 리스트의 개수를 그대로 사용했다. 정본일 수 있어 확인해 본 경로도, 복원에 반드시 필요한 경로도 같은 ‘없음’으로 계산된 것이다.
이 구조가 유지되는 동안 누락 카운트는 늘 1 이상이다. 정상 보관 상태에서도 0이 될 수 없으니, 경보만으로 실제 정본 유실을 구별하기 어려워진다. 정본을 놓치지 않으려던 수정이 경보의 의미를 바꿨는데, 우리는 바뀌기 전의 의미로 읽었다.
5. 재발 방지
개선안은 다음과 같다. 아직 적용하지 않았다. 세 항목 모두 팩 소유 파일을 바꾸는 일이어서 오너 승인 없이 수정하지 않았으며, 아래 문장은 완료 보고가 아니라 제안이다.
첫째, 후보와 필수를 분리해 센다. 후보는 정본을 찾기 위해 확인할 경로이고, 필수는 실제로 보관돼야 하는 파일이다. 대체 후보의 부재는 정상일 수 있으므로 정본 누락과 같은 경보에 넣지 않는다.
둘째, manifest.json에 missing 배열을 기록한다. 개수와 함께 경로 이름이 남아야 다음에는 ‘무엇이 빠졌는가’를 백업 기록에서 추적할 수 있다. 누락이라는 결과만 저장하고 대상은 버리는 빈틈을 메우자는 제안이다.
셋째, 문서의 pack/round와 코드의 _round 중 어느 쪽을 정본으로 삼을지 확정한다. 두 후보를 모두 모은 것은 어긋남을 덮은 조치였다. 기준을 하나로 맞추는 일이 남아 있다.
같은 날, 다른 판정에서도 나타난 어긋남
2026-09-07에는 아래 네 사례가 관측됐다. 표의 ‘게이트’는 다음 단계로 넘어가도 되는지 판정하는 검사다. stall은 작업 정체를 뜻하고, mtime은 파일의 수정 시각이며, clear는 대화 컨텍스트를 비우는 작업이다.
| 게이트 | 실제로 잰 것 | 판정하려던 것 | 오판 방향 |
|---|---|---|---|
| stall 게이트 | 진행 지표의 정지 | 노드의 무응답 | 정상을 실패로 |
| 저장 검증 | 파일 mtime의 변화 | 상태가 최신인지 | 정상을 실패로 |
| 스냅샷 누락 | 후보 경로의 부재 | 정본의 유실 | 정상을 실패로 |
| clear 완료 | 입력이 타이핑됐는지 | 컨텍스트가 비었는지 | 실패를 성공으로 |
앞의 세 사례는 정상을 실패로 오판했다. 이 관측에서는 시끄러운 경보가 났지만 손실은 없었다. 네 번째는 방향이 달랐다. 이 건은 운영을 담당하는 CSO 노드의 자기보고를 받아 사후 실측으로 확인했다. 도구가 컨텍스트 비움 완료를 선언했을 때 실제 사용률은 81% 그대로였고, 입력한 명령은 큐에서 대기 중이었다.
작업이 끝난 뒤 큐가 처리돼 결과적으로는 성공했다. 그러나 나중의 성공이 앞선 완료 선언의 근거가 되지는 않는다. 계속 바빴다면 그 선언은 실제 상태와 끝내 맞지 않았을 것이다. 이 건은 ‘조급한 성공 선언’이라고 적는 편이 정확하다.
지표는 변화를 재고 목적은 상태다. 둘이 갈라지면 변화 없는 정상이 실패로 판정된다. 반대로 명령을 입력했다는 신호를 원하는 상태가 됐다는 증거로 쓰면, 끝나지 않은 작업에 성공을 선언하게 된다.
이번 백업 도구는 후보의 부재를 정확히 세었다. 우리는 그 값을 정본의 유실로 받아들였다. 측정은 정확했고 의미만 틀렸다. 그래서 고칠 대상은 숫자만이 아니다. 그 숫자로 무엇을 판정해도 되는지부터 바로잡아야 한다.
근거 안내: 이 글의 사건·수치·로그·경로와 코드 동작은 우리 팀이 정리한 사실 브리프의 실측 기록을 출처로 삼았다. 1~5절과 같은 날 사례 표는 해당 브리프의 동명 절에 대응한다. 집필자가 과거 측정을 독립 재실행했다는 뜻은 아니다. 컨텍스트 비움 사례는 자기보고를 받아 사후 확인한 기록이고, 재발 방지 세 항목은 미적용 제안이다.