기본개념 · 8장
스키마와 직렬화
Kafka는 바이트 배열만 주고받습니다. 그 바이트를 무엇으로 해석할지에 대한 합의가 스키마이고, 그 합의를 시간에 걸쳐 안전하게 바꾸는 규칙이 호환성 모드입니다. 이 장의 핵심 산출물은 어떤 스키마 변경이 어떤 모드에서 허용되고 거부되는지의 매트릭스, 그리고 그 매트릭스에서 곧바로 따라 나오는 배포 순서입니다 — BACKWARD면 컨슈머 먼저, FORWARD면 프로듀서 먼저입니다.
학습 목표
- JSON · Avro · Protobuf · JSON Schema를 스키마 진화·크기·도구 관점에서 비교해 선택할 수 있습니다.
- Schema Registry의 subject · version · schema id 관계와 wire format 바이트 레이아웃을 설명할 수 있습니다.
- subject naming strategy 3종이 만드는 subject 이름을 계산할 수 있습니다.
- 호환성 모드 7종이 각각 어느 방향으로 무엇을 비교하는지 말하고, 변경 유형별 허용/거부를 판정할 수 있습니다.
- 배포 순서를 모드에서 유도할 수 있고, 뒤바꿨을 때 무엇이 깨지는지 설명할 수 있습니다.
auto.register.schemas가 프로덕션에서 위험한 이유를 설명할 수 있습니다.
직렬화 형식 비교
Kafka의 Serializer/Deserializer는 무엇이든 될 수 있습니다.
선택 기준은 스키마 진화를 감당할 수 있는가와 페이로드 크기,
그리고 팀이 쓰는 언어 생태계입니다.
| 관점 | JSON (스키마 없음) | Avro | Protobuf | JSON Schema |
|---|---|---|---|---|
| 인코딩 | 텍스트 | 바이너리 (스키마를 알아야 읽힘) | 바이너리 (필드 번호 기반) | 텍스트 |
| 페이로드 크기 | 가장 큼 (필드 이름이 매번 반복) | 가장 작음 (필드 이름이 페이로드에 없음) | 작음 (필드 번호만) | 가장 큼 |
| 스키마 진화 | 없음. 코드가 알아서 감당 | 가장 강력. reader/writer 스키마 해석(resolution) 규칙이 명세로 정의됨 | 강력. 필드 번호로 매칭하므로 이름 변경이 안전 | 보통 |
| 필드 식별 | 이름 | 이름 (+ reader 측 aliases) |
번호(tag) | 이름 |
| 필드 이름 변경 | 깨짐 | 위험. aliases로만 완화(reader 쪽에만 효과) |
안전 (번호가 같으면 됨) | 깨짐 |
| 필드 삭제 | — | default 유무에 따라 다름 | 번호를 reserved로 남겨 재사용을 막아야 함 |
— |
| 사람이 읽을 수 있는가 | 예 | 아니요 | 아니요 | 예 |
| Kafka 생태계 지원 | 기본 StringSerializer로 충분 |
가장 성숙. Connect·Streams·ksqlDB 전부 1급 지원 | 성숙 | 지원됨 |
| 쓰는 곳 | PoC, 로그, 외부 공개 API | 기본 선택. 데이터 파이프라인·CDC·분석 | gRPC와 스키마를 공유하는 마이크로서비스 | 이미 JSON Schema로 계약을 관리하는 조직 |
Schema Registry 아키텍처
Schema Registry는 스키마를 중앙에 보관하고 정수 ID를 발급하는 HTTP 서비스입니다. 핵심 흐름은 네 단계입니다.
- 프로듀서가 직렬화 시점에 스키마를 subject 아래로 등록합니다. Registry가 schema id를 돌려줍니다.
- 프로듀서는 메시지 앞에 그 id를 붙여 Kafka에 씁니다. 스키마 본문은 붙이지 않습니다.
- 컨슈머는 메시지 앞의 id를 읽어 Registry에 스키마를 조회합니다(그리고 캐시합니다).
- 조회한 writer 스키마와 컨슈머가 가진 reader 스키마로 역직렬화합니다.
subject · version · schema id
| 개념 | 무엇인가 | 범위 |
|---|---|---|
| subject | 스키마가 진화하는 이름 공간. 호환성 규칙이 적용되는 단위 | 기본 전략에서는 <topic>-key / <topic>-value |
| version | subject 안에서 1부터 증가하는 순번. 호환성 검사의 비교 대상을 정할 때 씁니다 | subject 내부에서만 의미가 있습니다 |
| schema id | 스키마 본문에 부여되는 정수 ID. 메시지에 실려 나갑니다 | Registry 전역. 같은 스키마는 여러 subject에서 같은 id를 공유합니다 |
wire format 바이트 레이아웃
직렬화된 메시지의 앞부분은 고정된 구조를 갖습니다.
Confluent 직렬화기의 소스에서 확인한 값은 MAGIC_BYTE_V0 = 0x0,
ID_SIZE = 4입니다.
바이트 0 1 2 3 4 5 ...
┌──────┬──────────────────────┬─────────────────────────┐
│ 0x00 │ schema id (4 bytes) │ 직렬화된 payload │
└──────┴──────────────────────┴─────────────────────────┘
│ │ │
│ │ └─ Avro binary / Protobuf wire / JSON 본문
│ └─ 부호 있는 32비트 정수, big-endian (ByteBuffer.putInt 기본)
└─ magic byte. 0x00 = "4바이트 schema id 형식"
예시 (schema id = 42, Avro payload 가 이어짐)
00 00 00 00 2A
^^ ^^^^^^^^^^^
magic 42
Protobuf 는 schema id 뒤에 message index 배열이 추가됩니다
(한 .proto 파일 안의 여러 message 중 어느 것인지 지시).
subject naming strategy 3종
subject 이름을 어떻게 만들지가 호환성 검사의 단위를 결정합니다. Confluent 직렬화기 소스에서 확인한 세 전략의 산출 규칙입니다.
| 전략 | subject 이름 | 호환성 검사 단위 | 쓰는 곳 |
|---|---|---|---|
TopicNameStrategy기본 동작 |
<topic>-key / <topic>-value |
토픽마다 독립 | 한 토픽에 한 종류의 레코드. 대부분의 경우 |
RecordNameStrategy |
레코드의 fully-qualified 이름 (예: com.example.OrderCreated) |
레코드 타입마다 독립. 토픽과 무관 | 여러 토픽에 같은 타입이 흐르고, 그 타입의 진화를 한 곳에서 관리하고 싶을 때 |
TopicRecordNameStrategy |
<topic>-<레코드 이름> |
토픽 × 레코드 타입 | 한 토픽에 여러 이벤트 타입을 섞어 보내면서 토픽별로 다른 진화를 허용할 때 |
value.subject.name.strategy=io.confluent.kafka.serializers.subject.TopicRecordNameStrategy
key.subject.name.strategy=io.confluent.kafka.serializers.subject.TopicNameStrategy
호환성 모드 7종 — 무엇을 어느 방향으로 비교하는가
모드가 하는 일은 두 개의 축으로 완전히 결정됩니다.
① 검사 방향(누가 reader이고 누가 writer인지)과
② 비교 범위(직전 버전만인지 모든 버전인지)입니다.
Confluent Schema Registry의 CompatibilityChecker 소스가 이를 명시적으로 정의합니다.
| 모드 | 검사 방향 (소스 주석) | reader / writer | 비교 범위 |
|---|---|---|---|
BACKWARDRegistry 기본값 |
새 스키마가 이전 스키마로 생산된 데이터를 읽을 수 있는가 | reader = 새 스키마 writer = 직전 버전 |
직전 버전만 (validateLatest) |
BACKWARD_TRANSITIVE |
새 스키마가 이전의 모든 스키마로 생산된 데이터를 읽을 수 있는가 | reader = 새 스키마 writer = 모든 이전 버전 |
모든 이전 버전 (validateAll) |
FORWARD |
새 스키마로 생산된 데이터를 이전 스키마가 읽을 수 있는가 | reader = 직전 버전 writer = 새 스키마 |
직전 버전만 |
FORWARD_TRANSITIVE |
새 스키마로 생산된 데이터를 이전의 모든 스키마가 읽을 수 있는가 | reader = 모든 이전 버전 writer = 새 스키마 |
모든 이전 버전 |
FULL |
새 스키마가 forward이자 backward로 호환되는가 | 양방향 (mutual read) | 직전 버전만 |
FULL_TRANSITIVE |
새 스키마가 모든 이전 버전에 대해 양방향 호환되는가 | 양방향 | 모든 이전 버전 |
NONE |
검사하지 않습니다 (no-op validator) | — | — |
Avro 스키마 해석 규칙 — 매트릭스의 근거
허용/거부는 임의로 정해진 것이 아닙니다.
Confluent의 AvroSchema.isBackwardCompatible()은
Avro의 SchemaCompatibility.checkReaderWriterCompatibility(reader, writer)를 직접 호출합니다.
따라서 매트릭스의 모든 칸은 Avro의 해석 규칙에서 유도됩니다.
| 규칙 | 내용 |
|---|---|
| reader에만 있는 필드 | reader 필드에 default가 있어야 합니다. 없으면 READER_FIELD_MISSING_DEFAULT_VALUE로 거부됩니다.(예외: 필드 타입이 enum이고 enum default가 있으면 통과) |
| writer에만 있는 필드 | 조용히 무시됩니다. 아무 오류도 없습니다 |
| 필드 매칭 방법 | 이름으로 찾고, 없으면 reader 필드의 aliases로 writer 필드를 찾습니다. reader 쪽 alias만 유효합니다 |
| 타입 승격 (reader ← writer) | long ← int · float ← int, long · double ← int, long, float · bytes ← string · string ← bytes.역방향은 TYPE_MISMATCH |
| enum | writer의 모든 심볼이 reader의 심볼 집합에 있어야 합니다. 없으면 MISSING_ENUM_SYMBOLS.단 reader에 enum default가 있으면 통과합니다 |
| union | writer가 union이면 모든 분기를 reader가 읽을 수 있어야 합니다. reader에 없는 분기가 있으면 MISSING_UNION_BRANCH |
| 레코드·fixed 이름 | 이름이 같거나 reader의 aliases가 writer의 full name을 포함해야 합니다. 아니면 NAME_MISMATCH |
| 필드 순서 | 이름으로 매칭하므로 순서 변경은 영향이 없습니다 |
| 스키마 타입 불일치 | Avro ↔ Protobuf처럼 타입이 다르면 즉시 거부됩니다 (Incompatible because of different schema type) |
호환성 매트릭스 — 이 장의 핵심 산출물
위의 두 표를 결합하면 매트릭스가 계산됩니다. 각 칸의 판정 방법은 항상 같습니다 — BACKWARD는 reader=새 스키마, FORWARD는 reader=옛 스키마로 두고 Avro 해석 규칙을 적용합니다.
| 변경 유형 (v1 → v2) | BACKWARD | FORWARD | FULL | NONE | 판정 근거 |
|---|---|---|---|---|---|
| default 있는 필드 추가 | ✓ | ✓ | ✓ | ✓ | BACKWARD: reader(v2)에만 있는 필드에 default가 있음 → 통과. FORWARD: writer(v2)에만 있는 필드는 reader(v1)가 무시 → 통과 |
| default 없는 필드 추가 | ✕ | ✓ | ✕ | ✓ | BACKWARD: reader(v2)에만 있고 default 없음 → READER_FIELD_MISSING_DEFAULT_VALUE.FORWARD: writer(v2)의 추가 필드는 무시됨 |
| default 있는 필드 삭제 | ✓ | ✓ | ✓ | ✓ | BACKWARD: writer(v1)에만 있는 필드는 reader(v2)가 무시. FORWARD: reader(v1)에만 있지만 default가 있음 → 통과 |
| default 없는 필드 삭제 | ✓ | ✕ | ✕ | ✓ | BACKWARD: writer(v1)의 잉여 필드는 무시. FORWARD: reader(v1)에만 있고 default 없음 → 거부 |
타입 승격 int → long |
✓ | ✕ | ✕ | ✓ | BACKWARD: reader long ← writer int 승격 허용.FORWARD: reader int ← writer long → TYPE_MISMATCH |
타입 축소 long → int |
✕ | ✓ | ✕ | ✓ | 승격 방향이 뒤바뀐 경우. 위와 정확히 대칭입니다 |
타입 변경 string → int |
✕ | ✕ | ✕ | ✓ | 승격 경로에 없는 조합 → 양방향 TYPE_MISMATCH |
string ↔ bytes 변경 |
✓ | ✓ | ✓ | ✓ | Avro는 양방향 승격을 허용합니다 (bytes ← string, string ← bytes 모두) |
| enum 심볼 추가 | ✓ | ✕ | ✕ | ✓ | BACKWARD: reader(v2) 심볼 집합이 writer(v1)를 포함 → 통과. FORWARD: writer(v2)의 새 심볼이 reader(v1)에 없음 → MISSING_ENUM_SYMBOLS. 단 v1에 enum default가 있으면 ✓ |
| enum 심볼 삭제 | ✕ | ✓ | ✕ | ✓ | 위와 대칭. v2에 enum default가 있으면 BACKWARD도 ✓ |
| 필드 이름 변경 (default·alias 없음) | ✕ | ✕ | ✕ | ✓ | 삭제 + 추가로 취급됩니다. 양쪽 모두 reader에만 있는 default 없는 필드가 생김 → 거부 |
| 필드 이름 변경 + 새 이름에 alias | ✓ | ✕ | ✕ | ✓ | alias는 reader 쪽만 유효합니다. BACKWARD는 reader=v2이므로 v2의 alias가 작동. FORWARD는 reader=v1이고 v1에는 alias가 없음 → 거부 |
| 필드 이름 변경 + 양쪽 모두 default 있음 | ✓ | ✓ | ✓ | ✓ | 검사는 통과하지만 값이 옮겨지지 않습니다. 아래 경고 참조 |
T → ["null", T](nullable 로 변경, default null) |
✓ | ✕ | ✕ | ✓ | BACKWARD: reader union이 writer 분기 T를 포함 → 통과.FORWARD: writer union의 null 분기를 reader T가 못 읽음 → 거부 |
["null", T] → T(nullable 제거) |
✕ | ✓ | ✕ | ✓ | 위와 대칭 |
| 필드 순서 변경 | ✓ | ✓ | ✓ | ✓ | Avro는 이름으로 매칭합니다. 순서는 무관합니다 |
| 레코드 이름 변경 (alias 없음) | ✕ | ✕ | ✕ | ✓ | NAME_MISMATCH. reader의 aliases에 writer의 full name이 있어야 통과합니다 |
| 스키마 타입 변경 (Avro → Protobuf 등) |
✕ | ✕ | ✕ | ✓ | Registry가 방향 검사 이전에 스키마 타입 일치를 먼저 검사합니다 |
*_TRANSITIVE 모드는 위 표의 판정을 직전 버전 대신 모든 이전 버전에 대해 반복합니다.
한 버전이라도 거부되면 전체가 거부됩니다.
즉 비-TRANSITIVE에서 ✕인 것은 TRANSITIVE에서도 ✕이고,
비-TRANSITIVE에서 ✓라도 TRANSITIVE에서는 ✕일 수 있습니다.
배포 순서 — 매트릭스에서 곧바로 나옵니다
이것은 암기 사항이 아니라 모드의 정의에서 논리적으로 따라 나오는 결론입니다. 각 모드가 무엇을 보장하는지 다시 보면 순서가 자동으로 정해집니다.
BACKWARD면 왜 컨슈머 먼저인가
BACKWARD가 보장하는 것은 "새 스키마가 옛 스키마로 생산된 데이터를 읽을 수 있다"입니다. 보장되지 않는 것은 그 반대 — 옛 스키마가 새 데이터를 읽는 것입니다.
따라서 새 코드는 옛 데이터를 안전하게 읽을 수 있고, 옛 코드는 새 데이터를 못 읽습니다. 새 스키마의 데이터가 토픽에 나타나기 전에 컨슈머를 모두 새 버전으로 올려야 합니다. 컨슈머를 먼저 배포하면, 전환 기간 동안 새 컨슈머가 옛 데이터를 읽습니다 — 이것이 BACKWARD가 보장하는 방향입니다.
순서를 뒤바꿔 프로듀서를 먼저 배포하면, 아직 옛 스키마를 쓰는 컨슈머가 새 스키마의 데이터를 만나 역직렬화에 실패합니다. 예를 들어 v2에서 default 있는 필드를 추가했다면(BACKWARD ✓, FORWARD도 ✓라 안전하지만), v2에서 default 없는 필드를 추가한 경우 BACKWARD에서는 애초에 거부되고, 필드를 삭제한 경우(BACKWARD ✓, FORWARD ✕) 옛 컨슈머는 자신이 요구하는 필드를 새 데이터에서 찾지 못해 실패합니다.
FORWARD면 왜 프로듀서 먼저인가
FORWARD가 보장하는 것은 "새 스키마로 생산된 데이터를 옛 스키마가 읽을 수 있다"입니다. 즉 옛 컨슈머가 새 데이터를 안전하게 읽습니다.
그래서 프로듀서를 먼저 올려도 됩니다. 컨슈머는 아직 옛 버전이지만 새 데이터를 문제없이 처리합니다. 컨슈머는 여유를 두고 나중에 전환합니다.
순서를 뒤바꿔 컨슈머를 먼저 배포하면, 새 컨슈머가 아직 옛 스키마로 생산되는 데이터를 읽어야 합니다. 그런데 FORWARD는 그 방향(새 reader ← 옛 writer)을 보장하지 않습니다. v2에서 default 없는 필드를 추가한 경우(FORWARD ✓, BACKWARD ✕), 새 컨슈머는 옛 데이터에서 그 필드를 찾지 못하고 default도 없어 실패합니다.
| 모드 | 보장하는 방향 | 배포 순서 | 뒤바꾸면 |
|---|---|---|---|
BACKWARDBACKWARD_TRANSITIVE |
새 코드 → 옛 데이터 읽기 | 컨슈머 먼저 → 프로듀서 | 옛 컨슈머가 새 데이터를 만나 역직렬화 실패 |
FORWARDFORWARD_TRANSITIVE |
옛 코드 → 새 데이터 읽기 | 프로듀서 먼저 → 컨슈머 | 새 컨슈머가 옛 데이터를 만나 역직렬화 실패 |
FULLFULL_TRANSITIVE |
양방향 | 순서 무관 | 문제 없음. 대신 허용되는 변경이 가장 좁습니다(default 있는 필드의 추가/삭제만) |
NONE |
없음 | — | 런타임에 터집니다. Registry가 막아 주지 않습니다 |
안전한 스키마 진화 규칙
매트릭스에서 실무 규칙을 추출하면 다음과 같습니다.
Registry 기본값이 BACKWARD이므로 그 기준으로 정리합니다.
| 규칙 | 이유 |
|---|---|
| 필드를 추가할 때는 항상 default를 준다 | default 없는 추가는 BACKWARD에서 거부됩니다. default가 있으면 모든 모드에서 통과합니다 |
nullable 로 시작한다: ["null", T] + default: null |
나중에 없앨 때도 안전합니다. "언젠가 값이 없을 수 있다"를 스키마에 미리 표현합니다 |
| 필드 이름은 바꾸지 않는다 | 거부되거나, 통과하더라도 값이 default로 대체되어 사라집니다 |
| 타입은 넓히는 방향만 (BACKWARD 기준) | int → long → float → double. 축소는 BACKWARD에서 거부됩니다 |
| enum 에는 default 심볼을 둔다 | enum default가 있으면 심볼 추가/삭제 양방향이 완화됩니다. 처음 설계할 때 UNKNOWN 같은 값을 넣어 두세요 |
| 필드 삭제는 2단계로 | ① 먼저 쓰기를 멈추고(값을 default로 두고) 배포 → ② 컨슈머가 모두 그 필드를 안 쓰게 된 뒤 스키마에서 제거 |
| 이름 변경이 꼭 필요하면 3단계로 | ① 새 이름 필드를 default와 함께 추가 → ② 프로듀서가 두 필드를 모두 채움, 컨슈머는 새 필드로 전환 → ③ 옛 필드 제거 |
보관 기간이 길면 *_TRANSITIVE |
비-TRANSITIVE는 직전 버전만 봅니다. 7일 이상 보관하는 토픽에서 v1 데이터를 v4 코드가 읽어야 한다면 직전 검사로는 부족합니다 |
| 배포 전에 호환성 API로 미리 검증 | CI에서 POST /compatibility/subjects/{subject}/versions/latest로 검사하면 배포 후 실패를 막습니다 |
// v1
{
"type": "record",
"namespace": "com.example",
"name": "OrderCreated",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "amount", "type": "int" },
{ "name": "status", "type": { "type": "enum", "name": "Status",
"symbols": ["NEW", "PAID"],
"default": "NEW" } }
]
}
{
"type": "record",
"namespace": "com.example",
"name": "OrderCreated",
"fields": [
{ "name": "orderId", "type": "string" },
// ✓ 타입 승격 int → long : reader(v2) long ← writer(v1) int 허용 → BACKWARD OK
// (FORWARD 로는 거부됩니다 — 모드가 BACKWARD 임을 전제합니다)
{ "name": "amount", "type": "long" },
// ✓ enum 심볼 추가 : reader(v2) 심볼 집합이 writer(v1) 를 포함 → BACKWARD OK
// default 심볼이 있으므로 FORWARD 도 완화됩니다
{ "name": "status", "type": { "type": "enum", "name": "Status",
"symbols": ["NEW", "PAID", "REFUNDED"],
"default": "NEW" } },
// ✓ default 있는 필드 추가 : 모든 모드에서 통과
{ "name": "currency", "type": "string", "default": "KRW" },
// ✓ nullable + default null : 나중에 제거할 때도 안전합니다
{ "name": "couponId", "type": ["null", "string"], "default": null }
]
}
{
"type": "record",
"namespace": "com.example",
"name": "OrderCreated",
"fields": [
// ✕ 이름 변경 (orderId → order_id, alias 없음)
// → BACKWARD: reader(v2) 의 order_id 가 writer(v1) 에 없고 default 없음 → 거부
{ "name": "order_id", "type": "string" },
// ✕ default 없는 필드 추가 → BACKWARD 거부
// READER_FIELD_MISSING_DEFAULT_VALUE
{ "name": "channel", "type": "string" },
// ✕ 타입 축소 int → 유지했더라도, long → int 였다면 BACKWARD 거부
// TYPE_MISMATCH (reader int ← writer long 은 승격 경로가 아님)
{ "name": "amount", "type": "int" }
]
}
{
"name": "order_id",
"type": "string",
// alias 는 reader 쪽에서만 해석됩니다.
// reader(v2) 가 writer(v1) 의 orderId 필드를 이 alias 로 찾아냅니다 → BACKWARD OK.
// 반대로 FORWARD 는 reader 가 v1 이고 v1 에는 alias 가 없으므로 거부됩니다.
"aliases": ["orderId"]
}
직렬화기 설정과 REST API
KafkaAvroSerializer 설정
| 설정 | 기본값 | 의미 | 프로덕션 권장 |
|---|---|---|---|
schema.registry.url |
— | Registry 엔드포인트. 쉼표로 여러 개 지정 가능 | 이중화를 위해 여러 개 |
auto.register.schemas |
true | 직렬화 시 스키마가 없으면 자동 등록 | false — 아래 경고 참조 |
use.latest.version |
false | 자동 등록 대신 subject의 최신 버전을 사용 | auto.register.schemas=false와 함께 true를 고려 |
latest.compatibility.strict |
true | use.latest.version 사용 시 최신 버전과의 호환성을 엄격히 검사 |
기본값 유지 |
use.schema.id |
-1 (미사용) | 직렬화에 쓸 schema id를 고정 | 특수한 경우만 |
normalize.schemas |
false | 등록·조회 시 스키마를 정규화해 의미상 동일한 스키마의 중복 등록을 방지 | true 검토 — 불필요한 버전 증가를 막습니다 |
key.subject.name.strategy |
<topic>-key를 만드는 전략 |
키 스키마의 subject 이름 결정 | 기본값 유지 |
value.subject.name.strategy |
<topic>-value를 만드는 전략 |
값 스키마의 subject 이름 결정 | 한 토픽에 여러 타입이면 TopicRecordNameStrategy |
max.schemas.per.subject |
1000 | 클라이언트 로컬 캐시의 subject당 스키마 수 상한 | — |
specific.avro.reader |
false | GenericRecord 대신 생성된 클래스로 역직렬화 |
코드 생성 방식을 쓰면 true |
bootstrap.servers=kafka-1:9092,kafka-2:9092,kafka-3:9092
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer
schema.registry.url=http://schema-registry-1:8081,http://schema-registry-2:8081
# 스키마 등록은 CI/CD 에서만. 애플리케이션은 등록하지 않습니다.
auto.register.schemas=false
# 이미 등록된 최신 버전을 사용합니다.
use.latest.version=true
# 의미상 동일한 스키마의 중복 등록을 방지합니다.
normalize.schemas=true
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer
schema.registry.url=http://schema-registry-1:8081,http://schema-registry-2:8081
# 생성된 Avro 클래스로 역직렬화 (기본값 false → GenericRecord)
specific.avro.reader=true
Registry REST API — 주요 엔드포인트
| 메서드 · 경로 | 용도 |
|---|---|
GET /subjects | subject 목록 |
POST /subjects/{subject} | 이 subject에 해당 스키마가 이미 등록되어 있는지 조회 |
DELETE /subjects/{subject} | subject 삭제 |
GET /subjects/{subject}/versions | 버전 목록 |
POST /subjects/{subject}/versions | 스키마 등록 (호환성 검사가 여기서 수행됩니다) |
GET /subjects/{subject}/versions/{version} | 특정 버전 조회. {version}에 latest 사용 가능 |
GET /subjects/{subject}/versions/{version}/schema | 스키마 본문만 조회 |
DELETE /subjects/{subject}/versions/{version} | 특정 버전 삭제 |
GET /subjects/{subject}/versions/{version}/referencedby | 이 스키마를 참조하는 스키마 목록 |
POST /compatibility/subjects/{subject}/versions/{version} | 배포 전 호환성 검사. 등록하지 않고 판정만 받습니다 |
POST /compatibility/subjects/{subject}/versions | 모든 버전에 대한 호환성 검사 |
GET /config · PUT /config | 전역 호환성 레벨 조회·변경 |
GET /config/{subject} · PUT /config/{subject} | subject별 호환성 레벨 조회·변경 (전역 설정을 오버라이드) |
DELETE /config/{subject} | subject별 오버라이드 제거 → 전역 설정으로 복귀 |
GET /schemas/ids/{id} | schema id로 스키마 조회 (컨슈머가 실제로 호출하는 경로) |
GET /schemas/ids/{id}/subjects · /versions | 이 id를 쓰는 subject와 버전 |
GET /schemas/types | 지원하는 스키마 타입 목록 (AVRO / PROTOBUF / JSON) |
GET /mode · PUT /mode · PUT /mode/{subject} | Registry 동작 모드 (읽기 전용 등) |
# 새 스키마를 등록하지 않고 판정만 받습니다.
curl -sS -X POST \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"schema": "{\"type\":\"record\",\"name\":\"OrderCreated\",\"namespace\":\"com.example\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"}]}"}' \
http://schema-registry:8081/compatibility/subjects/orders-value/versions/latest
# {"is_compatible": true} 또는 {"is_compatible": false, "messages": [...]}
# subject 별 호환성 모드 확인 · 변경
curl -sS http://schema-registry:8081/config/orders-value
curl -sS -X PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"compatibility": "BACKWARD_TRANSITIVE"}' \
http://schema-registry:8081/config/orders-value
# 전역 기본값 확인 (Registry 기본값은 BACKWARD)
curl -sS http://schema-registry:8081/config
흔한 오해
시험 포인트 요약
확인 문제
호환성 매트릭스의 각 칸과 배포 순서, wire format 바이트 구성이 집중적으로 나옵니다.
이어서 볼 곳
- 9장 · Kafka Connect converter와 SMT, 내부 토픽, DLQ — 스키마가 실제로 쓰이는 곳.
- 7장 · 스토리지·리텐션·컴팩션 보관 기간이 길수록 옛 스키마의 데이터를 계속 읽어야 합니다.
- 케이스 9 · 배포 순서를 뒤바꿨다 BACKWARD 토픽에 프로듀서를 먼저 올렸을 때의 실제 전개.
- 예제 7 · Avro와 Schema Registry 동작하는 프로듀서·컨슈머와 스키마 진화 실험.
- CCDAK · Fundamentals 23% 도메인 압축 정리. 호환성 매트릭스 요약 포함.
- CCDAK 함정 사전 BACKWARD vs FORWARD, TRANSITIVE vs 비-TRANSITIVE 비교표.
공식 문서 출처
Schema Registry는 Apache Kafka 본체가 아니므로, 이 장의 사실 확인은 Confluent Schema Registry와 Apache Avro의 공개 소스에서 했습니다. 각 항목이 어디서 확인되었는지 명시합니다.
- Confluent — Schema Registry Concepts — subject · version · schema id 모델, Registry 아키텍처.
확인 근거:confluentinc/schema-registry의CompatibilityLevel(7종 열거),SchemaRegistryConfig의schema.compatibility.level기본값backward - Confluent — Wire Format — magic byte + schema id + payload.
확인 근거:io.confluent.kafka.serializers.schema.id.SchemaId의MAGIC_BYTE_V0 = 0x0·MAGIC_BYTE_V1 = 0x1·ID_SIZE = 4,idToBytes()/guidToBytes()/fromBytes()구현, Protobuf의 message index 추가분,PrefixSchemaIdSerializer - Confluent — Subject Name Strategy — 3종 전략.
확인 근거:TopicNameStrategy(topic + "-key"/"-value"),RecordNameStrategy(레코드 FQN),TopicRecordNameStrategy(topic + "-" + recordName), 그리고KEY/VALUE_SUBJECT_NAME_STRATEGY_DOC의 "By default, <topic>-key is used as subject" 서술 - Confluent — Schema Evolution and Compatibility — 7개 모드와 배포 순서.
확인 근거:CompatibilityChecker의 소스 주석과 구성 —BACKWARD=canReadStrategy()+validateLatest()("새 스키마가 이전 스키마로 생산된 데이터를 읽을 수 있는지"),FORWARD=canBeReadStrategy()+validateLatest(),FULL=mutualReadStrategy(),*_TRANSITIVE=validateAll(),NONE=no-op. 그리고SchemaValidatorBuilder가 각 전략에서isBackwardCompatible의 인자 순서를 뒤집어 방향을 만드는 구현 - Apache Avro — Schema Resolution — 매트릭스의 모든 판정 근거.
확인 근거: AvroSchemaCompatibility의checkReaderWriterCompatibility·checkReaderWriterRecordFields(reader 전용 필드의 default 요구) ·lookupWriterField(reader 측 alias만 사용) ·schemaNameEquals(reader aliases) · 타입 승격 분기(LONG←INT,FLOAT←INT/LONG,DOUBLE←INT/LONG/FLOAT,BYTES↔STRING) ·checkReaderEnumContainsAllWriterEnumSymbols(enum default 예외) · union 분기 검사 ·SchemaIncompatibilityType6종(NAME_MISMATCH,FIXED_SIZE_MISMATCH,MISSING_ENUM_SYMBOLS,READER_FIELD_MISSING_DEFAULT_VALUE,TYPE_MISMATCH,MISSING_UNION_BRANCH). Confluent의AvroSchema.isBackwardCompatible()이 이 함수를(reader = this, writer = previousSchema)로 직접 호출하는 것도 확인했습니다 - Confluent — Schema Registry API Reference — REST 엔드포인트.
확인 근거:SubjectsResource(/subjects),SubjectVersionsResource(/subjects/{subject}/versions),CompatibilityResource(/compatibility/subjects/{subject}/versions/{version}),ConfigResource(/config,/config/{subject}),SchemasResource(/schemas/ids/{id},/schemas/types),ModeResource(/mode)의@Path·@GET/@POST/@PUT/@DELETE애너테이션 - Confluent — Serializer/Deserializer Configs — 직렬화기 설정 기본값.
확인 근거:AbstractKafkaSchemaSerDeConfig의AUTO_REGISTER_SCHEMAS_DEFAULT = true,USE_LATEST_VERSION_DEFAULT = false,LATEST_COMPATIBILITY_STRICT_DEFAULT = true,USE_SCHEMA_ID_DEFAULT = -1,NORMALIZE_SCHEMAS_DEFAULT = false,MAX_SCHEMAS_PER_SUBJECT_DEFAULT = 1000 - Apache Kafka — Producer Configs —
key.serializer/value.serializer규약