바이너리 형식 — 컬럼 지향과 스키마 진화
런타임 데이터 파일(.tcb)의 형식과, 스키마가 바뀌었을 때 무엇이 보장되는지를 적습니다.
.tcb는 Tabbit Compiled Binary입니다 — 이 도구가 빌드해 낸 런타임 데이터라는 뜻이고,
무엇에서 빌드했는지는 이름에 없습니다. 안에 든 것은 행이 아니라 컬럼별로 모인 값 블록과,
그 앞에서 각 블록의 길이를 먼저 적어 두는 디스크립터입니다.
바이트 순서는 리틀엔디안이고, 형식 버전은 107 하나뿐입니다 — 다른 버전은 그 자리에서 거부합니다.
이 형식을 쓰는 이유(파싱이 없다 · 인코딩이 값 크기에 맞는다 · 값이 변하지 않는다)는 내보내기에 있습니다.
확장자 · 바이트 순서 · 형식 버전
확장자는 익스포터와 테이블 리더 양쪽에서 recipe로 바꿀 수 있습니다 — FileExtension과
BinaryTableFileExtension이고, 설정에 있습니다. 유니티처럼 확장자를
가리는 곳에서는 .bytes로 둡니다. 안에 든 바이트는 파일 이름과 무관하게 같습니다.
고정폭 값(fixed32 · fixed64)은 항상 리틀엔디안으로 기록되고, 테이블 리더는 호스트의
순서가 아니라 그 순서로 해독합니다 — 빅엔디안 기기에서 읽어도 같은 값이 나옵니다. varint는
낮은 7비트부터 나가므로 정의상 리틀엔디안이고, uuid만 예외적으로 .NET Guid 배치(앞 세
구성요소는 리틀엔디안, 뒤 8바이트는 그대로)를 따릅니다.
옛 배치로 가는 호환 경로는 없고 만들 계획도 없습니다. 이 빌드가 못 읽는 파일 은 해석할 파일이 아니라 다시 뽑을 파일입니다. 버전이 올라간 자리는 이렇습니다.
| 버전 | 무엇이 달라졌나 |
|---|---|
| 102 | 디스크립터가 인코딩 바이트를 갖게 되었습니다 (결정) |
| 103 | 컬럼이 「로우마다 값이 있는지」를 담습니다. 그 비트를 검사하지 않는 리더는 presence 비트맵을 값으로 읽습니다 (결정) |
| 104 | 인코딩이 4종 늘었습니다. 모르는 인코딩 번호에서 리더가 멈추므로 감지되지 않는 오류는 아니지만, 블록에 닿기 전에 버전에서 멈추는 편이 낫습니다 (결정) |
| 105 | 인코딩이 하나 늘고 presence 비트맵이 인코딩 바이트를 갖습니다. 그 바이트를 모르는 리더는 비트맵의 첫 바이트로 읽으므로, 이쪽은 에러 없이 잘못 읽는 경우입니다 (결정) |
| 106 | 컬럼이 「배열의 어느 원소에 값이 있는지」를 담습니다. 와이어 바이트의 마지막 예약 비트인 비트 7이 그것을 선언하고, 모르는 리더는 원소 비트맵을 값 블록의 앞부분으로 읽습니다 (결정) |
| 107 | 고정 길이 배열 종류를 없앴습니다. 배열은 하나가 되었고 디스크립터의 원소 개수 필드도 빠졌습니다 — 길이가 파일에 한 번만 적히면 그 길이가 생성 코드에 상수로 굳고, 그러 면 컬럼을 하나 더하는 것이 데이터 패치가 아니라 코드 배포가 됩니다 (결정) |
값 인코딩이 받은 영향
값 인코딩은 구글 프로토콜 버퍼의 와이어 포맷을 따릅니다. 거의 그대로 씁니다.
| 따른 것 | 프로토버프에서 |
|---|---|
| 바이트당 7비트를 낮은 자리부터 쓰고, 더 있으면 최상위 비트를 세우는 varint | Base 128 Varints |
| 부호를 접어 작은 음수도 짧게 만드는 지그재그 | Signed Integers |
varint로 손해인 값은 고정폭으로 쓴다는 구분(fixed32·fixed64) | Non-varint Numbers |
| 길이를 앞에 적어 내용은 몰라도 건너뛸 수 있게 하는 방식 | Length-Delimited Records |
| 모르는 필드를 건너뛸 수 있으면 스키마가 바뀌어도 읽을 수 있다는 wire type 개념 자체 | Message Structure |
차이점은 그 태그를 어디에 붙이느냐 하나입니다.
프로토콜 버퍼는 값마다 붙이고, 이 형식은 컬럼마다 한 번 붙입니다. 정적 테이블의 행은 동종이라 스키마를 행 수만큼 반복할 이유가 없기 때문입니다.
그 차이를 레이아웃에 적었습니다.
프로토콜 버퍼가 주는 것 중 정적 테이블에 필요 없는 것은 아예 넣지 않았습니다.
임의 깊이의 중첩 메시지, oneof, 맵입니다.
전체 인코딩 규격은 Protocol Buffers — Encoding에 있습니다. 이 문서의 「값 인코딩」과 나란히 읽으면 무엇이 같고 무엇이 다른지 바로 보입니다.
이 형식이 해결하는 문제
행 지향이고 스키마가 파일에 없으면, 테이블 리더는 생성 시점에 알던 순서 그대로 읽는 수밖에 없습니다. 그러면 컬럼을 추가하든 지우든 순서를 바꾸든 테이블 리더는 한 칸 밀리고 그 뒤 전부가 어긋납니다 — 실패가 아니라 조용히 다른 값입니다.
클라이언트와 데이터를 항상 함께 배포한다면 문제가 없습니다. 문제는 그렇게 못 하는 흐름입니다.
- CDN 에 데이터만 올려 패치하는 경우
- 스토어 심사 중이거나 업데이트를 미룬 구버전 클라이언트가 새 데이터를 받는 경우
- 데이터만 롤백해서 신버전 클라이언트가 옛 데이터를 받는 경우
이 형식의 불변식은 하나입니다: 어떤 스키마 변경도 감지되지 않는 읽기 오류가 되지 않습니다. 읽을 수 있으면 정확히 읽고, 읽을 수 없으면 필드 이름과 양쪽 타입과 함께 멈춥니다.
이 문서의 나머지
| 무엇 | 어디 |
|---|---|
| 파일 레이아웃 | 헤더 · 디스크립터 · 값 블록이 파일에 놓이는 차례와 그 크기 |
| 컬럼 인코딩 | 값이 컬럼마다 어떤 형태로 접히는지 — 13종과 그 고르는 규칙 |
| 암호화와 변조 검출 | ChaCha20 봉투와 MAC. 무엇을 막고 무엇을 못 막는지 |
| 스키마가 바뀔 때 | 컬럼을 더하고 지우고 타입을 바꿀 때 무엇이 보장되는지 |