DB를 써봤다면 "로그"라는 단어를 이미 알고 있습니다 — 트랜잭션 로그, WAL(Write-Ahead Log) 말이죠. 이 챕터의 목표는 딱 하나입니다. "DB에서는 로그가 본체(테이블)를 보조하는 조연이지만, Quipu-Log에서는 로그가 곧 주연 — 데이터베이스 그 자체"라는 발상의 전환을 당신 머릿속에 심는 것.
Quipu-Log는 데이터를 고치지 않습니다. 오직 파일 끝에 덧붙이기(append)만 합니다. 이 단순한 규칙 하나가 크래시 안전성·변조 탐지·간단한 백업을 거의 공짜로 가져다줍니다.
먼저, 당신이 이미 아는 것: DB의 WAL
관계형 DB가 UPDATE accounts SET balance = 100 WHERE id = 42를 실행할 때, 실제로는 두 군데에 씁니다.
- WAL (로그) — "42번 계좌 잔액을 100으로 바꾼다"는 의도를 파일 끝에 순차적으로 먼저 적습니다. 빠릅니다(끝에 덧붙이기만 하니까).
- 데이터 페이지 (본체) — 실제 테이블이 저장된 B-tree 페이지를 찾아가, 그 자리의 값을 덮어씁니다. 이건 디스크 여기저기를 건드리는 느린 작업이라 나중에 천천히 합니다.
WAL이 먼저인 이유(=Write-Ahead)는 크래시 때문입니다. 본체를 고치다 정전이 나도, WAL에 "의도"가 남아 있으면 재부팅 후 그걸 다시 실행(replay)해서 복구할 수 있죠. WAL은 "진실의 원본(source of truth)"이고, 데이터 페이지는 그 진실을 빠르게 읽기 위한 캐시에 가깝습니다.
은행 통장을 떠올리세요. 통장에는 거래가 한 줄씩 추가될 뿐, 이미 찍힌 줄을 지우거나 고치지 않습니다. "현재 잔액"은 맨 아래 한 줄에 보여주지만, 그건 사실 모든 거래를 더한 결과일 뿐입니다. 거래 내역(로그)이 원본이고, 잔액(현재 상태)은 그걸 요약한 것이죠.
발상의 전환: 본체를 아예 없애버리면?
여기서 Quipu-Log의 결정적인 선택이 나옵니다. "데이터 페이지(본체)를 아예 만들지 말고, WAL만 남기면 어떨까?"
감사 로그는 애초에 "한번 일어난 일을 기록"하는 도구입니다. 일어난 사건은 나중에 바뀌지 않죠 — "어제 alice가 문서를 지웠다"는 사실은 영원히 그대로입니다. 그러니 값을 덮어쓸 일이 없습니다. 덮어쓸 일이 없다면, 덮어쓰기를 지원하는 무거운 B-tree 본체도 필요 없습니다.
DB에서는 WAL이 본체(테이블)를 위한 보조 장치다. Quipu-Log에서는 그 WAL 하나가 곧 데이터베이스다. "로그 = 데이터"라는 등식이 이 라이브러리 전체를 떠받치는 1번 기둥이다.
어떻게: 파일 끝에 프레임을 덧붙인다
그럼 실제로 파일에 무엇이 쌓일까요? 저장 엔진의 모듈 설명을 그대로 보겠습니다. Quipu-Log는 기록 하나하나를 프레임(frame)이라는 고정된 틀에 담아 파일 끝에 붙입니다.
crates/quipu-core/src/storage/mod.rs — 모듈 문서// 세그먼트 파일은 헤더로 시작하고(ALOG 매직 + 포맷 버전 + base index),
// 모든 레코드는 아래 프레임으로 감싸인다:
[u32 LE payload length][u32 crc32(payload)][u64 timestamp][payload]
// ↑ 본문 길이 ↑ 본문 체크섬 ↑ 기록 시각 ↑ 실제 내용
한 줄씩 뜯어보면, 왜 이 네 조각이 이 순서로 있는지가 보입니다.
payload length— 본문이 몇 바이트인지. 읽을 때 "어디까지가 이 레코드인가"를 알려줍니다. 다음 레코드의 시작을 찾는 이정표죠.crc32(payload)— 본문의 체크섬(일종의 짧은 지문). 디스크가 비트를 깨먹었거나 쓰다 만 경우, 읽을 때 다시 계산한 값과 안 맞으면 "이 레코드는 손상됐다"를 압니다. 9장에서 자세히timestamp— 기록 시각. 본문이 아니라 프레임 헤더에 둔 게 포인트입니다. 본문을 풀어보지(역직렬화하지) 않고도 시각만 훑어서 "이 파일은 3월치다" 같은 판단을 빠르게 할 수 있게요. 17장 리텐션에서 활용payload— 실제 감사 기록 내용. 10장 직렬화
그리고 이 모든 건 오직 std::fs(Rust 표준 파일 API)로만 이뤄집니다. 특별한 OS 기능이나 DB 엔진이 없어요. 그래서 모듈 문서가 자랑스럽게 적어둔 한 줄:
// std::fs / std::io 만 사용하므로 Rust가 지원하는 모든 OS에서 동작이 동일하다.
왜 하필 append-only인가 — 감사 로그와의 찰떡궁합
덧붙이기만 하는 규칙은 단순해 보이지만, 감사 로그가 원하는 성질을 줄줄이 공짜로 줍니다.
| append-only라서… | 그래서 얻는 것 |
|---|---|
| 이미 쓴 바이트를 절대 건드리지 않는다 | 변조 탐지가 쉬워진다. 과거를 고치는 건 "정상 동작"이 아니므로, 고친 흔적은 곧 사고의 증거다 (파트 5). |
| 쓰기가 항상 "파일 끝에 추가"다 | 빠르다. 디스크 헤드가 여기저기 점프할 필요 없이 순차 쓰기. |
| 옛 데이터는 절대 안 바뀐다 | 백업이 안전하다. 데몬이 돌아가는 중에도 옛 파일을 그냥 복사하면 된다 (락 걱정 없음). |
| 마지막 레코드만 미완성일 수 있다 | 크래시 복구가 단순하다. 파일 끝의 망가진 한 조각만 잘라내면 끝 (12장). |
본체(B-tree 인덱스)를 버린 대가는 읽기입니다. "42번 문서의 기록"을 찾으려면 원칙적으로 로그를 훑어야(scan) 하죠. Quipu-Log는 이걸 보조 인덱스로 해결하는데, 그게 어떻게 가능한지는 15장 인덱싱·16장 쿼리 실행에서 다룹니다. 지금은 "쓰기는 단순·빠르고, 읽기는 따로 손을 써야 한다"만 기억하세요.
"덮어쓰기를 코드가 아예 지원하지 않는다"는 건 강력한 보안 속성입니다. 버그로 과거 기록이 망가질 통로가 처음부터 없으니까요. 다만 이건 애플리케이션 레벨의 약속일 뿐, 디스크를 직접 만질 수 있는 공격자는 파일을 고칠 수 있습니다. 그걸 탐지하는 게 머클 트리(파트 5)의 일입니다 — append-only는 변조를 막는 게 아니라 변조가 비정상임을 분명히 합니다.
정리
- DB는 로그(원본) + 본체(빠른 읽기)를 둘 다 둔다.
- Quipu-Log는 로그 하나만 둔다. 감사 기록은 고칠 일이 없으니 본체가 불필요하다.
- 기록은
[길이][CRC][시각][본문]프레임으로 파일 끝에만 붙는다.std::fs만으로. - append-only는 빠른 쓰기·쉬운 백업·단순한 복구·변조 탐지 친화성을 준다. 대신 읽기는 따로 인덱스가 필요하다.
① "WAL이 곧 데이터베이스"라는 말을, 통장 비유를 써서 한 문장으로 설명해 보세요.
② Quipu-Log가 B-tree 데이터 페이지를 만들지 않는 이유는? (힌트: 감사 기록의 본질적 성질 하나 때문)
③ 프레임에서 timestamp를 본문 안이 아니라 헤더에 둔 이유는?