Schema Registry — 메시지 구조를 안전하게 바꾸는 방법
producer가 {"id":1,"name":"alice"}를 보내고 consumer가 읽는다. 그런데 producer가 필드를 추가했다 — {"id":1,"name":"alice","email":"..."}. 옛 consumer는 이걸 못 읽고 크래시난다. Kafka는 value를 바이트로만 저장하고 구조(schema)를 모른다. producer와 consumer가 각자 해석하다가 구조가 어긋나면 깨진다. 이걸 푸는 게 Schema Registry다.
이 글은 Schema Registry가 뭔지부터 시작해, 실제로 어떻게 설정하고 사용하는지(Schema Registry 기동, Avro schema 정의, producer/consumer serde 설정, 호환성 검사)까지 다룬다.
중요 전제: Schema Registry는 Confluent 확장이며 Apache Kafka core가 아니다. 별도 설치가 필요하다.
Kafka가 스키마를 모른다는 것 — 왜 문제인가
Kafka는 value를 바이트로만 저장한다. 그래서:
- 스키마(필드명·타입)는 producer/consumer 코드에만 존재.
- 스키마 변경(필드 추가/삭제)이 양쪽에 동시 적용되지 않으면 consumer 크래시.
- "스키마 안 바꾸면 되지" → 현실에선 불가능(요구사항 변화). 안전하게 바꾸는 규칙이 필요.
Schema Registry란 — 스키마를 별도로 두고 진화 규칙을 강제
Schema Registry는 스키마를 저장·버전 관리·호환성 검사하는 외부 서비스다(기본 포트 8081). 메시지를 보낼 때:
- producer가 스키마를 Registry에 등록.
- Registry가 호환성을 검사 → 통과하면 schema ID 발급.
- 메시지엔 schema ID만 붙여 보냄(
0x00 + ID + payload). - consumer가 ID로 Registry에서 스키마를 조회해 역직렬화.
이렇게 하면 "새 스키마가 옛 consumer를 깨뜨리는지"를 등록 시점에 검사해서, 불호환 스키마 등록 자체를 거부한다. 단순 문서화가 아니라 운영 가드레일이다.
Avro — 스키마와 데이터를 분리하는 포맷
Avro의 핵심은 스키마와 데이터 분리다. 데이터 자체엔 필드명이 안 들어감(스키마로 해석) → 크기 작음. 진화 규칙(필드 default 등)을 지원한다.
Schema Registry 설정 — 어떻게 기동하나
Schema Registry는 Confluent 확장이라 Kafka core 배포판에 없다. Docker로 기동하는 게 가장 쉽다:
Docker로 Schema Registry 기동
# Kafka(KRaft)가 이미 localhost:9092에서 기동 중이라 가정
# Schema Registry 컨테이너 기동
docker run -d --name schema-registry \
-p 8081:8081 \
-e SCHEMA_REGISTRY_HOST_NAME=localhost \
-e SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=PLAINTEXT://host.docker.internal:9092 \
-e SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081 \
confluentinc/cp-schema-registry:latest
또는 Confluent Platform 전체를 Docker Compose로:
# docker-compose.yml (발췌)
services:
schema-registry:
image: confluentinc/cp-schema-registry:latest
ports:
- "8081:8081"
environment:
SCHEMA_REGISTRY_HOST_NAME: schema-registry
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: "kafka:9092"
SCHEMA_REGISTRY_LISTENERS: "http://0.0.0.0:8081"
depends_on:
- kafka
기동 확인:
curl http://localhost:8081/subjects
# [] (빈 배열 — 아직 등록된 스키마 없음)
Avro 스키마 정의 — 메시지 구조를 JSON으로
Avro 스키마는 JSON으로 정의한다. User 레코드 예:
{
"type": "record",
"name": "User",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": "string", "default": ""}
]
}
type: record— Avro 레코드(구조체).fields— 필드 배열. 각 필드는 이름·타입·(선택)default.default: ""— 이 필드가 없는 옛 데이터를 읽을 때 기본값 사용 → 호환성의 핵심.
스키마 등록과 호환성 검사
스키마 등록
# v1 등록
curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"schema": "{\"type\":\"record\",\"name\":\"User\",\"fields\":[{\"name\":\"id\",\"type\":\"int\"},{\"name\":\"name\",\"type\":\"string\"}]}"}' \
http://localhost:8081/subjects/users-value/versions
# → {"id": 1} (schema ID 발급)
호환성 위반 시나리오
- v1:
{id: int, name: string}(email 없음). - v2 시도:
{id: int, name: string, email: string}(default 없음) → BACKWARD 위반 → Registry가 거부(409 Conflict). - v2':
{id: int, name: string, email: string, default: ""}→ default로 BACKWARD 호환 → 등록 성공(새 ID 발급).
이 "등록 시점 검사"가 단순 문서화가 아니라 운영 가드레일인 이유다.
호환성 모드
| 모드 | 의미 | 전제 |
|---|---|---|
| BACKWARD | 새 스키마가 옛 데이터를 읽을 수 있음 | 흔한 기본. consumer 먼저 업그레이드 |
| FORWARD | 옛 consumer가 새 데이터를 읽을 수 있음 | producer 먼저 업그레이드 |
| FULL | backward + forward | 가장 안전 |
| NONE | 검사 없음 | 위험 |
subject 명명 전략
- TopicNameStrategy(기본):
<topic>-key/<topic>-value. 한 topic = 한 스키마 타입. - RecordNameStrategy: Avro record 이름 기준. 한 topic에 여러 타입 허용(이벤트 소싱에 유리).
producer/consumer Avro serde 설정
Schema Registry를 쓰려면 producer와 consumer가 Avro serde를 써야 한다.
producer 설정 (Java)
# Avro 직렬화 + Schema Registry
value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer
schema.registry.url=http://localhost:8081
# (선택) 등록 시 호환성 확인 안 함(개발용)
auto.register.schemas=true
producer가 send()할 때 Avro 객체를 직렬화하며 자동으로 Schema Registry에 스키마를 등록/조회한다. 메시지엔 0x00(magic byte) + schema ID(4바이트) + Avro payload가 붙는다.
consumer 설정 (Java)
value.deserializer=io.confluent.kafka.serializers.KafkaAvroDeserializer
schema.registry.url=http://localhost:8081
specific.avro.reader=true # GenericRecord 대신 생성된 클래스 사용
Maven 의존성
<dependency>
<groupId>io.confluent</groupId>
<artifactId>kafka-avro-serializer</artifactId>
<version>7.7.0</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.11.0</version>
</dependency>
serde 불일치 주의: Schema Registry serializer로 쓰고 plain deserializer로 읽으면(또는 역) "Unknown magic byte!" 에러. 양쪽 모두 Avro serde + 같은 Schema Registry여야 한다.
전체 워크플로우 — schema에서 consume까지
- 스키마 정의: Avro JSON으로
User레코드 작성(default 필드 포함). - Schema Registry 기동: Docker로 localhost:8081에 기동.
- 스키마 등록:
curl POST /subjects/users-value/versions→ schema ID 발급. - producer 설정:
KafkaAvroSerializer+schema.registry.url. - produce: producer가 Avro 객체를 보내면 자동 직렬화 + ID 부여.
- consumer 설정:
KafkaAvroDeserializer+ 같은 Registry. - consume: consumer가 ID로 스키마 조회 → 역직렬화 → Avro 객체 획득.
- 스키마 진화: v2에 default 필드 추가 → BACKWARD 호환 확인 → 등록.
흔히 묻는 것, 흔히 틀리는 것
| 오해 | 정정 |
|---|---|
| "Schema Registry는 Kafka core의 일부" | Confluent 확장. ASF 배포판에 없음 |
| "Avro는 압축 포맷" | 아님. 스키마 기반 직렬화 포맷 |
| "스키마 바꾸면 무조건 깨진다" | 호환성 모드 + 진화 규칙 지키면 안전 |
| "magic byte는 장식" | wire format 식별자. plain serde와 섞으면 "Unknown magic byte!" |
| "Schema Registry 없이 Avro 써도 된다" | 가능은 하나 schema ID/호환성 검사 불가 → 진화 관리가 수동 |
더 깊이
- Confluent wire format:
0x00+ schema ID(4B) + payload. 모든 메시지에 붙음. _TRANSITIVE모드: 직전 버전뿐 아니라 모든 과거 버전과 호환.- Avro 진화 규칙: 필드 추가→default 필수, 타입 widening(int→long)만, 필드명 변경은 비호환(alias로 일부 완화).
- 비-Confluent 대안: Apicurio Registry, AWS Glue Schema Registry.
요약 — 이 글의 결론
- Kafka = value를 바이트로만 저장. 스키마 모름 → 진화 문제.
- Schema Registry(Confluent 확장, 비 core): 스키마 저장·버전·호환성 강제(등록 시점 검사).
- 설정: Docker로 기동(포트 8081), Kafka broker에 연결.
- Avro: 스키마-데이터 분리(JSON 스키마 정의). 크기 작음. 진화 규칙(default).
- producer/consumer:
KafkaAvroSerializer/Deserializer+schema.registry.url. - 호환성: BACKWARD(흔한 기본)/FORWARD/FULL/NONE. 필드 추가 시 default 필수.
- 운영 표준: BACKWARD + consumer 먼저 배포 + default 필수.
생각해 볼 문제
- Kafka가 value를 바이트로만 본다는 것이 왜 스키마 문제를 만드는가?
- Schema Registry를 Docker로 기동하는 방법을 설명하라.
- Avro 스키마에서 필드 추가 시 default가 필요한 이유는?
- producer/consumer에 Avro serde를 설정하려면 어떤 프로퍼티가 필요한가?
- "Unknown magic byte!" 에러가 발생하는 원인과 해결책은?
- Schema Registry가 ASF core가 아닌 것이 왜 중요한가? 대안은?
참고
- Conduktor — Schema registry - 접근 2026-07-09
- Confluent Schema Registry docs - 접근 2026-07-09
- Confluent — Avro serializer 설정 - 접근 2026-07-09
- Avro spec(avro.apache.org), Protobuf, JSON Schema
'Tech Artifacts > Kafka' 카테고리의 다른 글
| Kafka - 11. kafka streams (0) | 2026.07.09 |
|---|---|
| Kafka - 10. kafka connect (0) | 2026.07.09 |
| Kafka - 08. delivery semantics (0) | 2026.07.09 |
| Kafka - 07. log retention (0) | 2026.07.09 |
| Kafka - 06. replication (0) | 2026.07.09 |
