LabHub
배우기 러닝패스 코스

ACK 전에 꺼진 간식 자판기 · 빈 로그는 최신 상태라는 뜻이 아니다 · 이론

빈 로그는 최신 상태라는 뜻이 아니다

LabHub 에서 이어서 보기

한 줄 요약

커서는 다시 받을 위치이고, 보관 경계는 그 위치가 아직 존재하는지 판단하는 증거입니다. 빈 응답을 받았다고 최신 상태라는 뜻은 아닙니다.

왜 이게 필요했나

동아리 간식 창고의 재고를 전광판에 표시한다고 합시다. 입고는 양수, 배부는 음수인 변화량으로 기록합니다. 창고 서버는 계속 작동했지만 전광판은 주말 동안 꺼져 있었습니다. 월요일에 마지막 처리 번호를 보내 재접속하면 평소처럼 누락 이벤트를 받을 것이라 기대합니다. 그런데 운영자가 디스크를 아끼려고 주말 로그의 앞부분을 지웠습니다. 서버가 남은 이벤트만 보내면 전광판은 그럴듯한 숫자를 만들지만 실제 재고와는 영원히 어긋납니다.

이것은 연결 실패가 아닙니다. TCP 연결이 열리고 HTTP 응답이 성공해도 과거가 없다는 사실은 달라지지 않습니다. 앞 모듈의 outbox는 아직 전달할 의도를 보존했습니다. 이번 모듈은 의도적으로 오래된 전달 내역을 버린 뒤에도 현재 상태를 복구하는 문제입니다. 로그를 무조건 영구 보관하는 것은 보관 비용을 피하지 못하고, 아무 상태나 초기값으로 바꾸는 것은 실제 업무를 잃습니다. 그래서 빠른 재생과 전체 상태 복구를 별개의 경로로 설계합니다.

어떻게 동작하나

실습에서는 스트림 번호를 0부터 증가시키고 아직 이벤트가 없는 마지막 번호를 -1로 표현합니다. last는 마지막으로 적용한 번호, floor는 서버가 아직 보관하는 가장 이른 번호입니다. 정상적으로 남은 이벤트는 floor부터 last까지 연속하며, 전부 잘린 경우 floor는 last+1입니다. 이것은 이 실습의 정수 계약입니다. 다른 제품의 시간 기반 ID나 파티션 오프셋에 그대로 대입하지 않습니다.

| 서버 상태 | 클라이언트 last | 판단 |
| --- | --- | --- |
| floor=4, last=8 | 3 | 4부터 재생 가능 |
| floor=4, last=8 | 2 | 3이 없으므로 스냅샷 필요 |
| floor=9, last=8 | 8 | 이미 최신, 빈 배치 정상 |
| floor=9, last=8 | -1 | 전체 로그가 잘렸으며 복구 필요 |

핵심 부등식은 클라이언트 last가 floor-1보다 작은가입니다. 같으면 다음 번호가 floor이므로 안전합니다. 작으면 적어도 하나의 이벤트가 빠집니다. 서버보다 앞선 커서도 정상으로 받아들이지 않습니다. 잘못된 저장소에 접속했거나 로컬 상태가 다른 세대일 수 있어 별도의 오류로 조사해야 합니다. 부등호 하나 때문에 데이터가 중복되거나 빠질 수 있으므로 경계값을 각각 시험합니다.

원본 저장소는 checkpoint, stock, events 세 테이블을 씁니다. events는 재생할 개별 변화, stock은 누적 재고, checkpoint는 세대·마지막 번호·보관 경계를 담습니다. trim(through)은 through 이하의 이벤트만 지우고 floor를 through+1로 바꿉니다. 재고와 last는 바꾸지 않습니다. 재생용 내역을 줄인 것과 이미 발생한 업무를 취소한 것은 다릅니다. 이벤트가 0개가 되어도 checkpoint 한 행은 남겨 두므로 처음부터 빈 창고와 과거가 잘린 창고를 구별할 수 있습니다.

조회할 때도 메타데이터와 이벤트를 하나의 읽기 트랜잭션에서 봅니다. 먼저 보관 범위가 충분하다고 판단한 뒤 다른 쓰기 작업이 로그를 자르고, 그다음 이벤트만 읽는 구조는 검사 시점과 사용 시점이 다릅니다. SQLite의 별도 연결 간 격리 설명을 바탕으로 같은 읽기 시점을 유지합니다. 라이브러리가 제공하는 격리를 실제로 쓰지 않으면 테이블을 나누어 저장한 이유가 사라집니다.

현장에서 만나는 모습

전광판이 매일 꺼지는 시간이 네 시간이고 재생 로그는 두 시간만 보관한다면, 재접속 구현을 아무리 다듬어도 자주 전체 복구가 필요합니다. 여기서는 제품이 허용할 최대 오프라인 시간, 이벤트 발생량, 스냅샷 작성·전송 시간을 함께 생각해야 합니다. 보관량을 늘리면 복구 빈도는 줄지만 저장 비용과 개인정보 보관 범위가 커질 수 있습니다. 반대로 스냅샷이 작은 서비스는 짧은 로그와 빠른 전체 복구가 더 단순할 수 있습니다.

진단 로그에는 단순히 연결 성공이라고 적지 말고 요청 세대, 클라이언트 last, 서버 floor와 last, 선택한 복구 경로를 남깁니다. 본 실습은 고객 개인정보 없이 정수 재고만 사용합니다. 실제 서비스라면 전체 상태를 로그에 출력하는 대신 식별 범위와 진단용 메타데이터를 최소화해야 합니다. 업무 데이터가 포함된 스냅샷은 아무 공개 링크에 놓을 파일이 아닙니다.

한 번의 replay는 최대 16개만 반환합니다. 이것은 전체 복구 완료를 보장하는 수가 아니라 호출 하나의 자원 예산입니다. 20개가 남았다면 첫 호출 뒤 네 개가 더 남아 있는 것이 정상입니다. 빈 배치와 누락 오류를 구분해야 호출자가 다음 행동을 고를 수 있습니다. 많은 양이 남았는데도 한 번의 성공을 전체 완료로 표시하면 진행률이 사용자를 속이게 됩니다.

다음 확인에서 할 것

이 모듈은 먼저 퀴즈로 경계를 판단하고, 뒤의 종합 실습에서 append·trim·replay를 구현합니다. 전부 잘린 로그에서 오래된 커서를 보내 오류가 나는지, 최신 커서에는 빈 배치가 오는지 비교합니다. 또 잘라낸 뒤 새 번호가 0으로 되돌아가지 않는지 검사합니다. 다음 이론에서는 복구에 쓸 재고와 커서를 같은 시점에 묶습니다.

참고: [SQLite 연결 간 격리](https://www.sqlite.org/isolation.html). floor와 last의 의미, 배치 크기, 오류 분류는 위 창고 실습이 정의한 응용 프로토콜입니다.