Kafka Shallow Dive - Schema Registry & Avro

Published:

Kafka는 byte만 취급한다. 그래서 “이 topic에 어떤 구조의 데이터가 있냐”는 합의가 필요하다. 이 합의를 코드 외부에서 안전하게 관리하는 게 Schema Registry의 역할.

왜 Schema Registry?

JSON으로 막 보내면 편한데, 결국 사고가 남:

  • Producer가 필드 이름 바꿈 → Consumer가 터짐
  • 필드 하나 추가했는데 옛 consumer가 처리 못 함
  • 타입이 문자열에서 숫자로 바뀌는 조용한 변화 → 런타임 터짐

해결책:

  1. 강타입 포맷 (Avro, Protobuf, JSON Schema)
  2. 중앙 저장소에 스키마 버전을 보관
  3. 변경 시 호환성 체크 강제

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 포트)
  • 내부 저장: _schemas compacted topic
  • Producer/Consumer는 메시지 헤더에 스키마 ID만 박고, 본문은 payload만
  • Schema Registry가 아니라 _schemas topic이 진짜 저장소 — 레지스트리 서버는 stateless에 가깝다

메시지 wire format

Avro/Protobuf로 직렬화된 메시지의 맨 앞 5바이트:

┌──────┬──────────────┬────────────────────────────┐
│ 0x00 │ schema id(4) │ serialized payload         │
└──────┴──────────────┴────────────────────────────┘
  • 0x00 magic 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 SchemaJSON최근 지원사람이 읽기 쉽지만 크기/속도 손해

현업에선 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새 스키마로 옛 데이터 읽기옛 스키마로 새 데이터 읽기용도
BACKWARDconsumer 먼저 배포
FORWARDproducer 먼저 배포
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 장애의 큰 덩어리 하나를 예방할 수 있다.

다음 주가 마지막 — 모니터링, 튜닝, 장애 케이스까지 운영 관점 총정리.