Schema Registry와 호환성: Avro/Protobuf/JSON Schema, evolution policy
왜 스키마 관리가 필요한가
Kafka는 본질적으로 바이트 스트림이다. 브로커는 메시지 내용을 이해하지 않고 전달만 한다. 프로듀서가 오늘 age 필드를 String에서 Integer로 바꿔버려도 Kafka는 아무 말도 하지 않는다. 문제는 컨슈머가 런타임에 역직렬화를 시도할 때 폭발한다.
대규모 파이프라인에서 이 문제는 세 가지 형태로 반복된다.
- 포맷 불일치: 프로듀서 A와 B가 같은 토픽에 다른 구조의 데이터를 쓴다.
- 스키마 변경 통보 부재: 업스트림 팀이 필드를 추가·삭제했지만 다운스트림 팀이 모른다.
- 재처리 실패: 6개월 전 데이터를 다시 읽으려니 당시 스키마를 아무도 모른다.
Schema Registry는 이 문제를 해결한다. 스키마를 중앙에서 버전 관리하고, 프로듀서가 등록 — 컨슈머가 조회하는 구조로 파이프라인 전체에 계약을 강제한다.
1. Schema Registry 아키텍처
Confluent Schema Registry는 별도 서버로 구동되며 REST API를 노출한다. 스키마 메타데이터는 Kafka 내부 토픽 _schemas에 저장된다.
쓰기 담당
읽기 / 쓰기 포워드
읽기 / 쓰기 포워드
2. Schema ID 수신
3. 메시지에 5바이트 헤더 + 페이로드 작성
4. Kafka 토픽에 발행
_schemas 저장
2. Schema Registry에서 스키마 조회 (캐시)
3. 페이로드 역직렬화
와이어 포맷: 5바이트 헤더
Confluent Schema Registry를 사용하는 모든 메시지는 동일한 5바이트 프리픽스를 갖는다.
[0x00] [Schema ID — 4바이트 big-endian] [실제 페이로드]
↑ ↑
매직 바이트 이 ID로 Schema Registry에서 스키마 조회매직 바이트 0x00은 Confluent 와이어 포맷임을 나타내는 식별자다. 컨슈머는 첫 바이트를 확인해 Schema Registry 포맷인지 판단한다.
2. 세 가지 스키마 포맷 비교
Apache Avro
Avro는 Kafka 생태계에서 가장 널리 쓰이는 포맷이다. 스키마를 .avsc 파일(JSON 형식)로 정의하며, 메시지 페이로드는 바이너리로 인코딩된다.
{
"type": "record",
"name": "User",
"fields": [
{ "name": "id", "type": "int" },
{ "name": "name", "type": "string" },
{ "name": "email", "type": ["null", "string"], "default": null }
]
}특징:
- 바이너리 인코딩: JSON 대비 약 2배 압축
- 동적 타이핑: 스키마를 외부에서 관리하며 메시지에 포함하지 않음
- Union 타입:
["null", "string"]으로 nullable 필드 표현 - 필드 진화가 쉬움: 기본값이 있는 필드 추가·삭제가 안전
주의: Union 타입에서 기본값의 타입은 Union의 첫 번째 타입과 일치해야 한다.
["null", "string"]의 기본값은null,["string", "null"]의 기본값은string이어야 한다.
Protocol Buffers (Protobuf)
.proto IDL을 사용하며 필드 번호(tag)로 필드를 식별한다. 필드 이름이 아닌 번호로 인코딩하기 때문에 필드 이름 변경이 안전하다.
syntax = "proto3";
message User {
int32 id = 1;
string name = 2;
string email = 3; // 새로 추가 — 안전
reserved 4; // 이 번호는 재사용 금지
}특징:
- 가장 빠른 직렬화: JSON보다 1.5~2배 압축, 직렬화 속도 최고
- 코드 생성: Java, Go, C++, Python 등에서 타입 안전성 확보
- 필드 번호 재사용 금지: 삭제한 필드 번호는
reserved로 예약해야 함 - Schema Registry 호환성: BACKWARD_TRANSITIVE 권장 (새 메시지 타입 추가는 항상 forward 호환이 아님)
JSON Schema
JSON을 그대로 사용하면서 스키마로 유효성을 검증한다. 텍스트 포맷이라 가독성이 좋지만 바이너리 포맷보다 느리고 크다.
{
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"email": { "type": "string" }
},
"required": ["id", "name"],
"additionalProperties": true
}특징:
- 텍스트 포맷: 디버깅, 외부 API 연동에 유리
additionalProperties가 진화 핵심:true면 새 필드 추가 시 기존 컨슈머가 무시하며 통과,false면 알 수 없는 필드 거부- 내장 호환성 검증 없음: Avro/Protobuf와 달리 포맷 자체에 호환성 강제 장치 없음; Schema Registry 규칙에 전적으로 의존
✅ 바이너리, 높은 압축률
✅ 진화 규칙 명확 (기본값)
⚠ IDL 필수 (.avsc)
⚠ Union 기본값 규칙 주의
✅ 다국어 코드 생성
✅ 필드 이름 변경 안전
⚠ IDL 필수 (.proto)
⚠ 필드 번호 재사용 금지
✅ 외부 API 연동 유리
✅ IDL 불필요
❌ 텍스트 = 용량 큼
❌ 호환성 강제 없음
3. 호환성 레벨: 무엇을 허용하고 무엇을 막는가
Schema Registry는 새 스키마를 등록할 때 기존 스키마와 호환되는지 자동으로 검증한다. 6가지 레벨이 있다.
실무 선택 기준
| 레벨 | 허용되는 변경 | 언제 사용하나 |
|---|---|---|
| BACKWARD (기본) | 기본값 있는 필드 추가, 필드 삭제 | 대부분의 Kafka 파이프라인 |
| FORWARD | 선택 필드 삭제, 필드 추가 | 프로듀서만 제어 가능한 환경 (드묾) |
| FULL | 기본값 있는 선택 필드만 추가·삭제 | 제약이 엄격한 레거시 통합 |
| BACKWARD_TRANSITIVE | BACKWARD + 모든 이전 버전과 호환 | 토픽 전체 재처리 요건이 있는 경우 |
| FORWARD_TRANSITIVE | FORWARD + 모든 이전 버전과 호환 | (거의 사용 안 함) |
| FULL_TRANSITIVE | 가장 제약적 | (거의 사용 안 함) |
기본값 BACKWARD를 쓰는 이유: 컨슈머가 언제든 토픽의 처음 오프셋부터 재읽기(rewind)하더라도 새 스키마로 과거 데이터를 안전하게 역직렬화할 수 있다. Kafka Streams는 BACKWARD 호환성만 지원한다.
4. 포맷별 안전한 변경과 금지 변경
Avro
안전한 변경 (BACKWARD 호환):
- 기본값이 있는 필드 추가:
{"name": "phone", "type": ["null","string"], "default": null} - 필드 삭제 (이미 읽고 있던 필드도 포함)
금지된 변경:
- 기본값 없이 필드 추가
- 필드 타입 변경 (
int→string) - 필드 이름 변경 (Avro는 위치 기반이 아닌 이름 기반 매칭 사용)
- Union에서
null제거 (nullable → non-nullable)
Protocol Buffers
안전한 변경:
- 새 필드 추가 (기존 코드는 알 수 없는 필드 번호를 무시)
- 필드 이름 변경 (태그 번호로만 식별)
- 메시지 타입 추가
금지된 변경:
- 삭제한 필드 번호 재사용 — 가장 위험. 기존 디코더가 다른 타입으로 오해
- 필드 타입 변경 (
int32→int64) - 필드 삭제 후
reserved로 선언하지 않는 것
message User {
int32 id = 1;
string name = 2;
// 삭제한 email 필드 — 번호 재사용 방지
reserved 3;
reserved "email";
// 새 필드는 다음 번호부터
string phone = 4;
}JSON Schema
안전한 변경 (additionalProperties: true 유지 조건):
- 선택적 필드 추가 (기존 컨슈머는 무시)
- enum 값 추가
금지된 변경:
additionalProperties: true→false전환 (기존 데이터에 추가 필드가 있으면 검증 실패)- 필드 타입 변경
- 선택 필드를 required로 변경
5. Subject 명명 전략
Subject는 Schema Registry에서 스키마 버전을 관리하는 단위다. 같은 Subject 내에서 버전이 증가하며 호환성을 검증한다.
user-eventsuser-events-valuecom.example.Usercom.example.User-valueevents + 레코드: com.example.Userevents-com.example.User-valueksqlDB 제한: ksqlDB는 TopicNameStrategy만 지원한다. 토픽당 하나의 스키마만 허용되므로, 여러 레코드 타입을 하나의 토픽에 섞는 패턴은 ksqlDB와 함께 사용하기 어렵다.
6. 프레임워크 연동
Apache Flink
Confluent Cloud for Apache Flink는 Avro, JSON_SR, Protobuf 세 가지를 모두 지원한다. 오픈소스 Apache Flink는 Avro만 지원한다.
CREATE TABLE kafka_source (
id INT,
name STRING,
email STRING
) WITH (
'connector' = 'kafka',
'topic' = 'user-events',
'properties.bootstrap.servers' = 'broker:9092',
'value.format' = 'avro-confluent',
'schema-registry.url' = 'http://schema-registry:8081'
);Flink는 쓰기 시 자동으로 스키마를 등록하고, 읽기 시 Schema ID로 스키마를 조회한다. 5바이트 Confluent 와이어 포맷을 기본으로 지원한다.
Apache Spark
Spark의 표준 from_avro()는 Confluent 와이어 포맷(매직 바이트 + Schema ID)을 처리하지 못한다. 커뮤니티 라이브러리인 ABRiS를 사용하거나 직접 5바이트를 파싱해야 한다.
// 직접 파싱 예시
val schemaId = ByteBuffer.wrap(bytes.slice(1, 5)).getInt
val payload = bytes.drop(5)
val schema = schemaRegistryClient.getSchemaById(schemaId)ksqlDB
CREATE STREAM user_events (
id INT,
name VARCHAR
) WITH (
KAFKA_TOPIC = 'user-events',
VALUE_FORMAT = 'AVRO' -- 또는 PROTOBUF, JSON_SR
);ksqlDB는 Schema Registry에서 스키마를 자동으로 발견하고, 쓰기 시 새 스키마를 자동으로 등록한다.
7. 프로덕션 운영 패턴
HA 구성
Schema Registry는 단일 Primary 아키텍처다. 3개 이상 인스턴스를 Kafka 기반 리더 선출(Confluent 4.0+)로 운영한다.
_schemas 토픽 권장 설정:
replication-factor=3,min.insync.replicas=2cleanup.policy=compact(키 기준 최신 값만 보존)unclean.leader.election.enable=false(데이터 손실 방지)
스키마 폭발 방지
Subject 수가 너무 많아지거나 버전이 무한 증가하면 _schemas 토픽이 커지고 Schema Registry 메모리가 늘어난다.
max.schemas.per.subject설정으로 버전 한도 관리- 사용 안 하는 Subject를 주기적으로 soft-delete (논리 삭제 후 영구 삭제)
- Schema Registry 메모리 사용량 및
_schemas토픽 크기를 모니터링
핵심 모니터링 지표
| 지표 | 의미 | 경고 기준 |
|---|---|---|
| 스키마 등록 실패율 | 호환성 위반 시도 | 0 이상 → 스키마 거버넌스 검토 |
| 스키마 조회 레이턴시 (p99) | 캐시 히트율 저하 | 100ms 초과 |
| 리더 선출 빈도 | 네트워크 불안정 또는 과부하 | 비정상적 빈번 → 즉시 조사 |
_schemas 토픽 크기 | 스키마 폭발 여부 | 1GB 초과 → 거버넌스 리뷰 |
References
- Confluent Documentation, "Schema Registry for Confluent Platform," https://docs.confluent.io/platform/current/schema-registry/index.html
- Confluent Documentation, "Schema Evolution and Compatibility," https://docs.confluent.io/platform/current/schema-registry/fundamentals/schema-evolution.html
- Conduktor, "Avro vs Protobuf vs JSON Schema: Kafka Serialization Compared," https://www.conduktor.io/glossary/avro-vs-protobuf-vs-json-schema
- AutoMQ Blog, "Avro vs. JSON Schema vs. Protobuf: Choosing the Right Format for Kafka," https://www.automq.com/blog/avro-vs-json-schema-vs-protobuf-kafka-data-formats
- Apache Avro, "Specification," https://avro.apache.org/docs/1.11.1/specification/
- DEV Community (Steven J), "Demystifying Confluent's Schema Registry Wire Format," https://dev.to/stevenjdh/demystifying-confluents-schema-registry-wire-format-5465
- Confluent Developer, "Schema Registry 101 - Schema Subjects," https://developer.confluent.io/courses/schema-registry/schema-subjects/
- Confluent Documentation, "Deploy Schema Registry in Production," https://docs.confluent.io/platform/current/schema-registry/installation/deployment.html
- Earthly Blog, "Backward and Forward Compatibility with Protocol Buffers," https://earthly.dev/blog/backward-and-forward-compatibility/
- AutoMQ Blog, "Which Kafka Schema Registry is Right for Your Architecture in 2026?," https://www.automq.com/blog/kafka-schema-registry-confluent-aws-glue-redpanda-apicurio-2025