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). 메시지를 보낼 때:

  1. producer가 스키마를 Registry에 등록.
  2. Registry가 호환성을 검사 → 통과하면 schema ID 발급.
  3. 메시지엔 schema ID만 붙여 보냄(0x00 + ID + payload).
  4. 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까지

  1. 스키마 정의: Avro JSON으로 User 레코드 작성(default 필드 포함).
  2. Schema Registry 기동: Docker로 localhost:8081에 기동.
  3. 스키마 등록: curl POST /subjects/users-value/versions → schema ID 발급.
  4. producer 설정: KafkaAvroSerializer + schema.registry.url.
  5. produce: producer가 Avro 객체를 보내면 자동 직렬화 + ID 부여.
  6. consumer 설정: KafkaAvroDeserializer + 같은 Registry.
  7. consume: consumer가 ID로 스키마 조회 → 역직렬화 → Avro 객체 획득.
  8. 스키마 진화: 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 필수.

생각해 볼 문제

  1. Kafka가 value를 바이트로만 본다는 것이 왜 스키마 문제를 만드는가?
  2. Schema Registry를 Docker로 기동하는 방법을 설명하라.
  3. Avro 스키마에서 필드 추가 시 default가 필요한 이유는?
  4. producer/consumer에 Avro serde를 설정하려면 어떤 프로퍼티가 필요한가?
  5. "Unknown magic byte!" 에러가 발생하는 원인과 해결책은?
  6. Schema Registry가 ASF core가 아닌 것이 왜 중요한가? 대안은?

참고

'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