Kafka Shallow Dive - Schema Registry & Avro
Published:
Kafka는 byte만 취급한다. 그래서 “이 topic에 어떤 구조의 데이터가 있냐”는 합의가 필요하다. 이 합의를 코드 외부에서 안전하게 관리하는 게 Schema Registry의 역할.
왜 Schema Registry?
JSON으로 막 보내면 편한데, 결국 사고가 남:
- Producer가 필드 이름 바꿈 → Consumer가 터짐
- 필드 하나 추가했는데 옛 consumer가 처리 못 함
- 타입이 문자열에서 숫자로 바뀌는 조용한 변화 → 런타임 터짐
해결책:
- 강타입 포맷 (Avro, Protobuf, JSON Schema)
- 중앙 저장소에 스키마 버전을 보관
- 변경 시 호환성 체크 강제
Schema Registry = 스키마의 git + 호환성 CI.
구조
┌──── Schema Registry ────┐
│ /subjects/orders-value │
│ v1 { id, amount } │
Producer ─ register ▶│ v2 { id, amount, │◀ fetch ─ Consumer
│ currency } │
└─────────────────────────┘
▲
│ (스키마 자체도 Kafka topic
│ `_schemas`에 저장)
- HTTP REST API (기본 8081 포트)
- 내부 저장:
_schemascompacted topic - Producer/Consumer는 메시지 헤더에 스키마 ID만 박고, 본문은 payload만
- Schema Registry가 아니라
_schemastopic이 진짜 저장소 — 레지스트리 서버는 stateless에 가깝다
메시지 wire format
Avro/Protobuf로 직렬화된 메시지의 맨 앞 5바이트:
┌──────┬──────────────┬────────────────────────────┐
│ 0x00 │ schema id(4) │ serialized payload │
└──────┴──────────────┴────────────────────────────┘
0x00magic byte- 4 bytes big-endian schema ID (Schema Registry가 발급한 전역 ID)
- 나머지 실제 데이터
Consumer는 ID 보고 레지스트리에서 스키마 fetch(캐시), 그 스키마로 역직렬화.
Avro vs Protobuf vs JSON Schema
| 포맷 | 크기 | 타입 시스템 | Kafka 전통 | 특징 |
|---|---|---|---|---|
| Avro | 작음 | 동적, JSON 스키마 파일 | 초기부터 표준 | 스키마 진화 규칙이 명확 |
| Protobuf | 작음 | 정적, proto 파일 | gRPC 써본 팀에게 친숙 | 언어 중립, 잘 알려진 생태계 |
| JSON Schema | 큼 | JSON | 최근 지원 | 사람이 읽기 쉽지만 크기/속도 손해 |
현업에선 Avro가 여전히 주류, gRPC와 함께 쓰는 팀은 Protobuf 선호. 크기/속도 차이는 Protobuf가 약간 우위지만 호환성 규칙이 Avro가 더 잘 문서화돼 있음.
Avro 예시
{
"type": "record",
"name": "Order",
"namespace": "com.example.shop",
"fields": [
{ "name": "id", "type": "long" },
{ "name": "amount", "type": "double" },
{ "name": "status", "type": { "type": "enum", "name": "Status",
"symbols": ["PENDING","PAID","CANCELED"] } }
]
}
Producer 설정:
value.serializer=io.confluent.kafka.serializers.KafkaAvroSerializer
schema.registry.url=http://schema-registry:8081
첫 send 시 레지스트리에 자동 등록되고 ID 받음. 이후부터는 캐시된 ID 사용.
스키마 호환성 모드
이게 핵심. 새 스키마를 올릴 때 “기존과 호환되냐”를 서버가 판단.
| Mode | 새 스키마로 옛 데이터 읽기 | 옛 스키마로 새 데이터 읽기 | 용도 |
|---|---|---|---|
| BACKWARD | ✅ | ❌ | consumer 먼저 배포 |
| FORWARD | ❌ | ✅ | producer 먼저 배포 |
| FULL | ✅ | ✅ | 둘 다 자유 |
| NONE | — | — | 체크 없음 (금지) |
| BACKWARD_TRANSITIVE | 전 버전 모두 back | 롱테일 consumer 대응 | |
| FORWARD_TRANSITIVE | 전 버전 모두 forward | ||
| FULL_TRANSITIVE | 전 버전 전부 | 전 버전 전부 | 가장 엄격 |
Kafka 기본은 BACKWARD. 즉 “새 consumer가 예전 메시지도 읽을 수 있어야 함”.
BACKWARD에서 허용되는 변경
- 필드 제거 (consumer 쪽에서 없어도 되니)
- 기본값 있는 필드 추가 (옛 메시지엔 기본값으로 채워짐)
허용 안 되는 변경:
- 기본값 없는 필수 필드 추가
- 필드 타입 변경 (int → string 등)
- enum 값 제거
변경 시나리오별 권장
- Consumer가 먼저 배포되고 Producer가 따라오는 구조 → BACKWARD (기본)
- Producer가 먼저 배포되는 구조 → FORWARD
- 양쪽 동시 → FULL
- 롱테일 consumer(오래된 버전까지 지원) →
_TRANSITIVE
호환성 모드는 topic(정확히는 subject) 단위로도 오버라이드 가능.
# 글로벌 모드 설정
curl -X PUT -H "Content-Type: application/json" \
--data '{"compatibility": "FORWARD"}' \
http://localhost:8081/config
# 특정 subject만
curl -X PUT -H "Content-Type: application/json" \
--data '{"compatibility": "FULL"}' \
http://localhost:8081/config/orders-value
Subject Naming Strategy
“이 topic의 스키마 이름”을 어떻게 지을지.
| 전략 | Subject 이름 | 한 topic에 여러 타입? |
|---|---|---|
| TopicName (기본) | <topic>-value, <topic>-key | ❌ |
| RecordName | <full-record-name> | ✅ (topic 무관) |
| TopicRecordName | <topic>-<record-name> | ✅ |
기본(TopicName)은 “한 topic에 한 스키마”. 다형 이벤트(“OrderCreated”랑 “OrderCanceled”를 한 topic에)를 쓰려면 RecordName/TopicRecordName이 필요.
실무 권장: 기본 TopicName 유지 + 정말 필요할 때만 다른 전략. 혼용하면 관리 복잡도 폭발.
스키마 진화 실전 팁
- 항상 default 넣기 — 새 필드 추가 시 기본값 안 주면 BACKWARD 깨짐
- enum 추가는 조심 — 일부 언어/버전에서 새 심볼을 모르면 터짐. string + 검증 로직이 안전한 경우도
- 필드 이름 rename 금지 — 삭제 + 추가로 처리
- 타입 변경 금지 — 새 필드 추가 후 마이그레이션 기간 거쳐 옛 필드 삭제
- CI에 호환성 체크 걸기 — Gradle/Maven 플러그인으로 레지스트리에 미리
test-compatibility요청 - 스키마도 코드 리뷰 대상 — PR에 포함되어야 함
CLI로 다루기
# subject 목록
curl http://localhost:8081/subjects
# 특정 subject의 버전들
curl http://localhost:8081/subjects/orders-value/versions
# 최신 스키마
curl http://localhost:8081/subjects/orders-value/versions/latest | jq
# 호환성 테스트 (등록은 안 함)
curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"schema": "<escaped-json-schema>"}' \
http://localhost:8081/compatibility/subjects/orders-value/versions/latest
실무에선 jq와 조합해서 구 버전 찾거나 호환성 배치 체크.
한계/주의
- Schema Registry는 Kafka 메시지 내용 검증. Kafka 프로토콜에는 관여 안 함
- ACL 등 보안은 별도. 기본은 평문 HTTP → 운영에선 TLS + 인증 필수
- 스키마 삭제는 soft delete → hard delete 2단계. 잘못 올리면 hard delete로 완전히 지워야 동일 버전 재등록 가능
- Protobuf/JSON Schema 호환성 규칙은 Avro와 다름. 포맷 별로 문서 확인
정리
Schema Registry는 “Kafka는 byte만 다룬다”의 공백을 메우는 컴포넌트. Avro + BACKWARD 호환성이 실무 기본 조합이고, 스키마 변경은 “필드 추가는 default와 함께, 제거는 BACKWARD로, rename은 금지”가 암묵의 룰. 스키마가 CI에 들어가면 production 장애의 큰 덩어리 하나를 예방할 수 있다.
다음 주가 마지막 — 모니터링, 튜닝, 장애 케이스까지 운영 관점 총정리.
