LabHub
배우기 러닝패스 코스

ACK 전에 꺼진 간식 자판기 · 재고와 커서를 같은 순간에 찍는다 · 이론

재고와 커서를 같은 순간에 찍는다

LabHub 에서 이어서 보기

한 줄 요약

스냅샷은 예쁜 JSON 파일이 아니라, 상태와 그 상태가 포함한 마지막 이벤트를 같은 시점에 묶는 약속입니다.

왜 이게 필요했나

창고 재고가 7이고 마지막 번호가 0일 때 전광판이 전체 복구를 요청했습니다. 서버가 먼저 last=0을 읽고, 그 사이 입고 5가 커밋됐다고 합시다. 이어 재고를 읽으면 total=12가 나옵니다. 각각의 값은 실제로 존재했던 값이지만, last=0과 total=12라는 조합은 존재한 적 없는 상태입니다. 이 스냅샷을 설치한 전광판은 1번 이벤트의 +5를 다시 받아 17을 표시합니다.

이 실수는 JSON 문법 검사, 타입 검사, 파일 해시 비교만으로 잡히지 않습니다. 서버가 직접 만든 JSON이어서 출처도 맞고, 숫자도 범위 안이며, 전송 중 바뀐 바이트도 없습니다. 잘못은 두 조회가 서로 다른 시점을 읽었다는 데 있습니다. 무결성 검사가 무엇을 확인하고 무엇을 확인하지 않는지 구별해야 합니다. 형식을 엄격하게 검증하는 것과 의미가 일관된 스냅샷을 만드는 것은 모두 필요합니다.

어떻게 동작하나

실습의 export_snapshot은 읽기 트랜잭션 안에서 checkpoint의 epoch·last를 먼저 읽고 stock.total을 읽습니다. 중간의 between 훅은 다른 연결이 새로운 이벤트를 커밋하도록 만드는 시험 지점입니다. 반환값은 version·epoch·last·total 네 키이며 version은 정수 1입니다. 같은 트랜잭션의 두 조회는 하나의 상태를 이루고, 트랜잭션을 끝낸 뒤 새로 조회하면 새 상태가 보이는지 별도로 확인합니다.

SQLite WAL 모드의 읽기 시점 격리를 사용합니다. 읽는 동안 다른 연결이 커밋해도 그 읽기 트랜잭션은 기존 시점을 유지할 수 있습니다. 반대로 export를 BEGIN IMMEDIATE로 시작하면 쓰기 자리를 선점하여 시험의 다른 연결을 막습니다. 원본 쓰기에는 직렬화가 필요하지만, 조회만 하는 내보내기가 모든 입고를 막아야 하는 것은 아닙니다. 언제 읽기 트랜잭션을 쓰고 언제 쓰기 트랜잭션을 쓰는지 의도가 달라야 합니다.

시험에서는 reader 연결의 첫 조회 뒤 writer 연결이 +5를 커밋하고, reader가 두 번째 조회를 합니다. 결과는 이전 커서와 이전 재고여야 하며, writer를 다시 보면 새 재고가 있어야 합니다. 가짜 sleep으로 운 좋게 경쟁이 발생하기를 기다리지 않습니다. 훅으로 조회 사이를 직접 지정하므로 같은 실수는 매번 같은 실패를 냅니다. 이는 동시 읽기와 쓰기의 논리적 겹침을 검증하는 것이지 많은 스레드의 처리량을 측정하는 부하 시험은 아닙니다.

내보낸 스냅샷을 복제본에 넣는 과정도 원자적이어야 합니다. 이전 events를 지우고 새 재고를 넣은 뒤 커서를 바꾸는 모든 작업이 한 트랜잭션입니다. 중간에 프로세스가 꺼지면 이전 상태가 온전히 남거나 새 상태가 온전히 남아야 합니다. old total과 new last가 섞이면 다음 재생이 누락됩니다. 커밋이 끝난 뒤 응답이 사라진 것은 설치가 없었다는 뜻이 아니므로, 같은 스냅샷 재설치는 상태를 바꾸지 않고 False를 반환하도록 설계합니다.

현장에서 만나는 모습

스냅샷을 만들기 위해 DB 파일 하나를 복사하는 접근은 논리 상태 내보내기와 다릅니다. WAL 모드에서는 별도 파일에 아직 반영되지 않은 확정 내용이 있을 수 있습니다. 이 실습은 원본 .db 파일을 복사하지 않고, 쿼리로 필요한 업무 상태만 가져옵니다. 실제 백업은 사용하는 DB의 백업 API와 복구 절차로 별도 검증해야 합니다. 전광판용 JSON을 만들었다는 이유로 서버 장애에서 복원할 백업도 갖췄다고 말하지 않습니다.

또 하나의 운영 문제는 읽기 시간을 무작정 늘리는 것입니다. 스냅샷을 네트워크로 전송하는 동안 트랜잭션을 잡아 두면 느린 수신자가 DB의 오래된 상태를 붙잡을 수 있습니다. 여기서는 작고 고정된 메타데이터와 정수 재고를 메모리에 읽은 뒤 트랜잭션을 닫습니다. 실제 대형 상태는 분할·체크섬·완성 표식·접근 권한·전송 재개까지 설계해야 합니다. 한 개의 정수 예제가 그 비용을 없애 주는 것은 아닙니다.

버전도 관찰 대상입니다. 확인한 Linux 실습 이미지의 SQLite 보고 버전은 3.45.1입니다. 공식 WAL 문서는 3.51.3 이후와 일부 백포트에서 WAL-reset 결함을 고쳤다고 설명합니다. 이 실습은 유한한 단일 쓰기 흐름이며 각 연결의 자동 체크포인트를 끄고, 동시 체크포인트 작업을 만들지 않습니다. 연결 종료도 쓰기가 끝난 뒤 순서대로 합니다. 이것은 다중 쓰기 운영 서버의 안전한 기본 설정을 제안하는 것이 아닙니다. 실제 배포에서는 배포판의 패치 포함 여부와 수정 릴리스를 확인해야 하며 버전 문자열 하나로 패치 상태를 단정하지 않습니다.

다음 확인에서 할 것

이어지는 퀴즈에서 어긋난 스냅샷과 쓰기 잠금의 차이를 판단합니다. 뒤의 종합 실습에서 export_snapshot과 install_snapshot을 구현합니다. 두 SELECT 사이 커밋을 잘못 끼워 넣으면 실제값 total=12와 기대값 total=7이 함께 나옵니다. 설치 과정의 after-clear·after-install 지점에는 예외와 실제 자식 종료를 각각 넣습니다. 예외 처리의 롤백이 맞는지와 정리 코드가 아예 실행되지 않아도 DB가 복구되는지를 구별해서 관찰합니다.

참고: [SQLite 읽기 시점 격리](https://www.sqlite.org/isolation.html), [WAL의 파일·동시성·알려진 결함](https://www.sqlite.org/wal.html). 이 실습의 재고 JSON은 데이터베이스 전체 백업이 아닙니다.