본문으로 건너뛰기

바이너리 형식 — 컬럼 지향과 스키마 진화

문서 목록으로

런타임 데이터 파일(.tcb)의 형식과, 스키마가 바뀌었을 때 무엇이 보장되는지를 적습니다.

.tcb는 Tabbit Compiled Binary입니다 — 이 도구가 빌드해 낸 런타임 데이터라는 뜻이고, 무엇에서 빌드했는지는 이름에 없습니다. 안에 든 것은 행이 아니라 컬럼별로 모인 값 블록과, 그 앞에서 각 블록의 길이를 먼저 적어 두는 디스크립터입니다.

바이트 순서는 리틀엔디안이고, 형식 버전은 107 하나뿐입니다 — 다른 버전은 그 자리에서 거부합니다.

이 형식을 쓰는 이유(파싱이 없다 · 인코딩이 값 크기에 맞는다 · 값이 변하지 않는다)는 내보내기에 있습니다.


확장자 · 바이트 순서 · 형식 버전

확장자는 익스포터와 테이블 리더 양쪽에서 recipe로 바꿀 수 있습니다 — FileExtensionBinaryTableFileExtension이고, 설정에 있습니다. 유니티처럼 확장자를 가리는 곳에서는 .bytes로 둡니다. 안에 든 바이트는 파일 이름과 무관하게 같습니다.

고정폭 값(fixed32 · fixed64)은 항상 리틀엔디안으로 기록되고, 테이블 리더는 호스트의 순서가 아니라 그 순서로 해독합니다 — 빅엔디안 기기에서 읽어도 같은 값이 나옵니다. varint는 낮은 7비트부터 나가므로 정의상 리틀엔디안이고, uuid만 예외적으로 .NET Guid 배치(앞 세 구성요소는 리틀엔디안, 뒤 8바이트는 그대로)를 따릅니다.

옛 배치로 가는 호환 경로는 없고 만들 계획도 없습니다. 이 빌드가 못 읽는 파일은 해석할 파일이 아니라 다시 뽑을 파일입니다. 버전이 올라간 자리는 이렇습니다.

버전무엇이 달라졌나
102디스크립터가 인코딩 바이트를 갖게 되었습니다 (결정)
103컬럼이 「로우마다 값이 있는지」를 담습니다. 그 비트를 검사하지 않는 리더는 presence 비트맵을 값으로 읽습니다 (결정)
104인코딩이 4종 늘었습니다. 모르는 인코딩 번호에서 리더가 멈추므로 감지되지 않는 오류는 아니지만, 블록에 닿기 전에 버전에서 멈추는 편이 낫습니다 (결정)
105인코딩이 하나 늘고 presence 비트맵이 인코딩 바이트를 갖습니다. 그 바이트를 모르는 리더는 비트맵의 첫 바이트로 읽으므로, 이쪽은 에러 없이 잘못 읽는 경우입니다 (결정)
106컬럼이 「배열의 어느 원소에 값이 있는지」를 담습니다. 와이어 바이트의 마지막 예약 비트인 비트 7이 그것을 선언하고, 모르는 리더는 원소 비트맵을 값 블록의 앞부분으로 읽습니다 (결정)
107고정 길이 배열 종류를 없앴습니다. 배열은 하나가 되었고 디스크립터의 원소 개수 필드도 빠졌습니다 — 길이가 파일에 한 번만 적히면 그 길이가 생성 코드에 상수로 굳고, 그러면 컬럼을 하나 더하는 것이 데이터 패치가 아니라 코드 배포가 됩니다 (결정)

값 인코딩이 받은 영향

값 인코딩은 구글 프로토콜 버퍼의 와이어 포맷을 따릅니다. 거의 그대로 씁니다.

따른 것프로토버프에서
바이트당 7비트를 낮은 자리부터 쓰고, 더 있으면 최상위 비트를 세우는 varintBase 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. 무엇을 막고 무엇을 못 막는지
스키마가 바뀔 때컬럼을 더하고 지우고 타입을 바꿀 때 무엇이 보장되는지