이 케이스에서 얻어 갈 것

상황

주문 이벤트 파이프라인입니다. orders 토픽은 파티션 24개, 복제 계수 3, 브로커 3대에서 일 500만 건을 받습니다. Avro + Schema Registry를 쓰고, subject는 기본 전략(TopicNameStrategy)에 따라 orders-value입니다.

이 토픽을 구독하는 컨슈머는 7개입니다 — 정산, 배송, 알림, 리스크, 검색 인덱서, 데이터 웨어하우스 싱크, 실시간 대시보드. 전부 생성된 Avro 클래스(SpecificRecord)를 씁니다.

변경 요청 — 통화 정밀도 문제로 금액 표현을 바꿔야 했습니다
// v1 (기존, 등록됨)
{
  "type": "record", "name": "Order", "namespace": "com.example.orders",
  "fields": [
    {"name": "orderId",       "type": "string"},
    {"name": "memberId",      "type": "string"},
    {"name": "amount",        "type": "long"},
    {"name": "legacyCouponId","type": "string"}
  ]
}

// v2 (배포하려던 것)
{
  "type": "record", "name": "Order", "namespace": "com.example.orders",
  "fields": [
    {"name": "orderId",  "type": "string"},
    {"name": "memberId", "type": "string"},
    {"name": "amount",   "type": "string"},   // ← long 에서 string 으로 타입 변경
    {"name": "currency", "type": "string"}    // ← default 없는 신규 필드
  ]
}
// legacyCouponId 제거 (default 없는 필드였음)

CI 파이프라인의 스키마 등록 단계가 409로 실패했습니다. 릴리스 창구가 닫히기 30분 전이었고, 담당자는 스택오버플로에서 본 대로 subject의 호환성 모드를 NONE으로 바꿨습니다.

실행된 우회 — 이 두 줄이 사고의 전부입니다
$ curl -sX PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \
    --data '{"compatibility": "NONE"}' \
    http://schema-registry:8081/config/orders-value
{"compatibility":"NONE"}

$ # 등록이 통과됨
$ curl -sX POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
    --data @order-v2.json \
    http://schema-registry:8081/subjects/orders-value/versions
{"id":1418}

그리고 프로듀서를 먼저 배포했습니다. "데이터의 원천이니까 프로듀서가 먼저"라는 것이 이유였습니다. 14:02에 배포가 끝나고, 14:13에 7개 컨슈머 전부가 재시작 루프에 들어갔습니다.

case09 — 스키마 호환성 모드를 낮춰 컨슈머 전체가 멈추는 흐름 정상 흐름에서는 기본 호환성 모드 BACKWARD 아래에서 새 스키마가 이전 스키마로 쓰인 데이터를 읽을 수 있는지 검사하고, 호환되지 않는 스키마는 등록 자체가 거부되므로 배포 사고가 사전에 막힙니다. 어긋나는 지점에서 급한 배포를 위해 호환성 모드를 NONE 으로 낮추면 기본값 없는 필수 필드를 추가한 스키마도 등록에 성공합니다. 결과적으로 이전 스키마를 쓰는 컨슈머가 새 레코드를 역직렬화하지 못해 예외를 던지고, 그 오프셋에서 더 나아가지 못한 채 같은 레코드를 반복 실패합니다. 컨슈머 그룹 전체가 같은 지점에서 멈추므로 장애 범위가 넓습니다. 처방은 호환성 모드를 BACKWARD 이상으로 유지하고 필드 추가 시 기본값을 주며 스키마 변경을 배포 전에 검사하는 것입니다. 1. 정상 흐름 호환성 검사가 방어선입니다 새 스키마 등록 요청 BACKWARD 검사 비호환이면 등록 거부 배포 전에 발견 BACKWARD 는 새 스키마로 이전 데이터를 읽을 수 있어야 통과시킵니다 — 컨슈머를 먼저 배포하는 전략과 짝입니다. 기본값이 있는 필드 추가나 필드 삭제처럼 안전한 변경만 통과합니다. 2. 어긋나는 지점 검사를 끄면 방어선이 사라집니다 호환성 NONE 으로 변경 default 없는 필수 필드 추가 등록 성공 프로듀서 먼저 배포 NONE 은 어떤 변경도 통과시킵니다 — 레지스트리가 더 이상 사고를 막아 주지 않습니다. 프로듀서가 먼저 배포되면 그 순간부터 새 형식의 레코드가 토픽에 쌓입니다. 3. 결과 그룹 전체가 같은 지점에서 멈춥니다 구 컨슈머가 새 레코드 수신 역직렬화 예외 오프셋 진행 불가 그룹 전체 정지 한 레코드에서 계속 실패하므로 뒤의 정상 레코드도 처리되지 않습니다 (포이즌 레코드). 재시작해도 같은 오프셋에서 다시 실패하고 lag 만 계속 늘어납니다. 처방 호환성 모드는 BACKWARD 이상으로 유지합니다 — 급하다고 NONE 으로 내리지 않습니다. 필드를 추가할 때는 기본값을 함께 정의해 이전 스키마 컨슈머가 읽을 수 있게 합니다. BACKWARD 계열은 컨슈머를 먼저, FORWARD 계열은 프로듀서를 먼저 배포합니다. 스키마 변경을 CI 에서 호환성 검사로 걸러 배포 파이프라인 밖으로 나가지 않게 합니다.
호환성 모드와 배포 순서 — BACKWARD에서 컨슈머를 먼저 올려야 하는 이유, FORWARD에서 프로듀서를 먼저 올려야 하는 이유, 그리고 순서를 뒤바꿨을 때 깨지는 지점

관측된 증상

메트릭이 어떻게 보였는가

컨슈머 로그 — 크래시 루프의 지문

Kafka 클라이언트가 역직렬화 실패를 감싸는 예외는 RecordDeserializationException이고, 메시지에 파티션과 오프셋이 정확히 찍힙니다. 이 오프셋이 재시작마다 같다는 것이 크래시 루프의 증거입니다.

settlement-service 로그 — 같은 오프셋에서 반복해서 죽습니다
ERROR Application shutting down due to unhandled exception
org.apache.kafka.common.errors.RecordDeserializationException: Error deserializing VALUE for partition orders-11 at offset 88413902. If needed, please seek past the record to continue consumption.
	at org.apache.kafka.clients.consumer.internals.CompletedFetch.newRecordDeserializationException(CompletedFetch.java:...)
Caused by: org.apache.kafka.common.errors.SerializationException: Error deserializing Avro message for id 1418
Caused by: org.apache.avro.AvroTypeException: Found string, expecting long
	at org.apache.avro.io.ResolvingDecoder.doAction(ResolvingDecoder.java:...)

레코드가 여러 건 섞여 있으면 다음 형태로 감싸져 나오기도 합니다. 같은 CompletedFetch가 출력합니다.

배치 중간에서 실패한 경우
org.apache.kafka.common.KafkaException: Received exception when fetching the next record from orders-11. If needed, please seek past the record to continue consumption.

그리고 원래 나왔어야 했던 오류

14:02 이전, CI가 받았던 409가 정확한 진단이었습니다. Schema Registry가 반환하는 본문은 다음 형태입니다.

호환성 위반 시 Schema Registry 응답 — 409 + 에러 코드 40901
$ curl -sX POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
    --data @order-v2.json \
    http://schema-registry:8081/subjects/orders-value/versions -w '\nHTTP %{http_code}\n'

{"error_code":40901,"message":"Schema being registered is incompatible with an earlier schema for subject \"orders-value\", details: [{errorType:'TYPE_MISMATCH', description:'The type (path \"/fields/2/type\") is not compatible ...'}]"}
HTTP 409

프로듀서 애플리케이션에서는 이 응답이 아래 형태의 예외로 나타납니다. RestClientException이 메시지 끝에 ; error code: 40901을 붙입니다.

프로듀서 쪽에서 보이는 형태 — 자동 등록을 켠 경우
org.apache.kafka.common.errors.SerializationException: Error registering Avro schema
Caused by: io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException: Schema being registered is incompatible with an earlier schema for subject "orders-value", details: [...]; error code: 40901

원인 분석

1단계 — 호환성 모드를 확인한다

전역 모드와 subject 모드를 모두 확인합니다
# 전역 설정
$ curl -s http://schema-registry:8081/config
{"compatibilityLevel":"BACKWARD"}

# subject 설정 — 여기가 오버라이드되어 있었습니다
$ curl -s http://schema-registry:8081/config/orders-value
{"compatibilityLevel":"NONE"}

# subject 의 버전 목록과 최신 스키마
$ curl -s http://schema-registry:8081/subjects/orders-value/versions
[1,2,3,4,5]
$ curl -s http://schema-registry:8081/subjects/orders-value/versions/latest | jq -r '.id, .version'
1418
5

# 문제의 스키마 ID 로 직접 조회 (로그에 찍힌 id)
$ curl -s http://schema-registry:8081/schemas/ids/1418 | jq -r '.schema' | jq .

2단계 — 호환성 모드 7종을 정확히 안다

Schema Registry 설정의 compatibility 유효 값과 그 정의입니다. 공식 설정 설명을 그대로 옮긴 것이므로 이 문장을 기준으로 판단하세요.

호환성 모드 7종. Schema Registry의 기본값은 backward입니다.
모드 정의 (공식 설명) 비교 대상 배포 순서
NONE 새 스키마는 유효한 스키마이기만 하면 된다 없음 보장 없음
BACKWARD
기본값
새 스키마가 최신 등록 스키마로 생산된 데이터를 읽을 수 있다 최신 버전 1개 컨슈머 먼저
BACKWARD_TRANSITIVE 새 스키마가 이전 모든 버전에 대해 backward 호환 모든 이전 버전 컨슈머 먼저
FORWARD 최신 등록 스키마가 새 스키마로 생산된 데이터를 읽을 수 있다 최신 버전 1개 프로듀서 먼저
FORWARD_TRANSITIVE 새 스키마가 이전 모든 버전에 대해 forward 호환 모든 이전 버전 프로듀서 먼저
FULL 최신 등록 스키마에 대해 backward + forward 모두 호환 최신 버전 1개 순서 무관
FULL_TRANSITIVE 이전 모든 버전에 대해 backward + forward 모두 호환 모든 이전 버전 순서 무관

3단계 — 배포 순서는 정의에서 그대로 따라 나온다

4단계 — 이 변경이 어느 방향으로도 호환되지 않는 이유

v1 → v2 변경은 세 가지였습니다. 각각을 방향별로 따져 봅니다. 리더(reader) = 읽는 쪽의 스키마, 라이터(writer) = 데이터를 쓴 쪽의 스키마입니다.

변경 항목별 호환성 판정 (Avro 스키마 해석 규칙 기준)
변경 BACKWARD
(새 리더 ← 옛 데이터)
FORWARD
(옛 리더 ← 새 데이터)
amount: longstring 불가
타입이 맞지 않음
불가
타입이 맞지 않음
currency 추가 (default 없음) 불가
옛 데이터에 값이 없고 default도 없음
가능
옛 리더가 모르는 필드를 무시
legacyCouponId 제거 (default 없던 필드) 가능
새 리더가 그 필드를 요구하지 않음
불가
옛 리더가 요구하는데 새 데이터에 없고 default도 없음

5단계 — 안전한 스키마 진화 규칙

Avro 스키마 변경 유형별 호환 방향
변경 BACKWARD FORWARD 메모
필드 추가 — default 있음 가능 가능 가장 안전한 변경. FULL 호환
필드 추가 — default 없음 불가 가능 기본값을 붙이면 FULL이 됩니다
필드 제거 — default 있던 필드 가능 가능 제거 전에 default를 붙여 두는 것이 요령
필드 제거 — default 없던 필드 가능 불가 이 사고의 legacyCouponId
필드 타입 변경 불가 불가 승격 가능한 조합(intlong 등) 외에는 불가. 새 필드를 추가하는 방식으로 우회
필드 이름 변경 (alias 없음) 불가 불가 제거 + 추가와 같습니다. aliases를 쓰면 완화 가능
enum 심볼 추가 조건부 조건부 enum default 지정 여부에 따라 달라집니다

직렬화 형식·Schema Registry 아키텍처·wire format의 상세는 8장 스키마와 직렬화에서 다룹니다.

재현 방법

브로커와 Schema Registry가 함께 필요합니다. 브로커는 Apache Kafka의 공식 단일 노드 예제를 쓰고, Schema Registry 컨테이너를 추가합니다 (3노드 구성은 예제 1, Avro 진화 실습은 예제 7).

docker-compose.yml — 브로커 + Schema Registry
services:
  broker:
    image: apache/kafka:4.3.1
    hostname: broker
    container_name: broker
    ports:
      - '9092:9092'
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: 'broker,controller'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
      KAFKA_LISTENERS: 'CONTROLLER://:29093,PLAINTEXT://:19092,PLAINTEXT_HOST://:9092'
      KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://broker:19092,PLAINTEXT_HOST://localhost:9092'
      KAFKA_CONTROLLER_QUORUM_VOTERS: '1@broker:29093'
      KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
      KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
      CLUSTER_ID: '4L6g3nShT-eMCtK--X86sw'
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'

  schema-registry:
    image: confluentinc/cp-schema-registry:latest   # 사용 중인 CP 버전으로 고정하세요
    hostname: schema-registry
    container_name: schema-registry
    depends_on: [broker]
    ports:
      - '8081:8081'
    environment:
      SCHEMA_REGISTRY_HOST_NAME: schema-registry
      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'PLAINTEXT://broker:19092'
      SCHEMA_REGISTRY_LISTENERS: 'http://0.0.0.0:8081'
      SCHEMA_REGISTRY_KAFKASTORE_TOPIC_REPLICATION_FACTOR: 1
재현 절차 — 409가 안전장치임을 확인한 뒤 우회해서 깨뜨립니다
docker compose up -d
SR=http://localhost:8081
CT='Content-Type: application/vnd.schemaregistry.v1+json'

# 0. 기본 호환성 모드 확인 → BACKWARD
curl -s $SR/config

# 1. v1 스키마 등록
cat > v1.json <<'EOF'
{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"namespace\":\"com.example.orders\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"long\"},{\"name\":\"legacyCouponId\",\"type\":\"string\"}]}"}
EOF
curl -sX POST -H "$CT" --data @v1.json $SR/subjects/orders-value/versions

# 2. v2(타입 변경 + default 없는 필드 추가 + 필드 제거) 등록 시도 → 409
cat > v2.json <<'EOF'
{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"namespace\":\"com.example.orders\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"string\"},{\"name\":\"currency\",\"type\":\"string\"}]}"}
EOF
curl -sX POST -H "$CT" --data @v2.json $SR/subjects/orders-value/versions -w '\nHTTP %{http_code}\n'
#   → {"error_code":40901,"message":"Schema being registered is incompatible with an earlier
#      schema for subject \"orders-value\", details: [...]"}
#     HTTP 409     ★ 이것이 안전장치입니다

# 3. 등록 전에 호환성만 검사하는 엔드포인트 (CI 에서 이걸 써야 합니다)
curl -sX POST -H "$CT" --data @v2.json \
  "$SR/compatibility/subjects/orders-value/versions/latest?verbose=true"
#   → {"is_compatible":false,"messages":[...]}

# 4. 사고 재현 — 모드를 NONE 으로 바꿔 우회
curl -sX PUT -H "$CT" --data '{"compatibility":"NONE"}' $SR/config/orders-value
curl -sX POST -H "$CT" --data @v2.json $SR/subjects/orders-value/versions
#   → {"id":N}  등록 성공. 이 순간 사고가 확정됩니다.

# 5. 컨슈머(v1 리더)가 v2 데이터를 읽으면 실패합니다.
#    kafka-avro-console-producer / consumer 로 확인할 수 있습니다.
docker exec -it schema-registry kafka-avro-console-producer \
  --bootstrap-server broker:19092 --topic orders \
  --property schema.registry.url=http://schema-registry:8081 \
  --property value.schema.id=N          # 4단계에서 받은 id
# {"orderId":"ORD-1","amount":"34900","currency":"KRW"}

# v1 스키마를 리더로 지정해 읽으면 역직렬화가 실패합니다
docker exec -it schema-registry kafka-avro-console-consumer \
  --bootstrap-server broker:19092 --topic orders --from-beginning \
  --property schema.registry.url=http://schema-registry:8081
모드별 배포 순서를 직접 확인하는 실험
# BACKWARD 에서는 "default 있는 필드 추가"가 통과합니다
curl -sX PUT -H "$CT" --data '{"compatibility":"BACKWARD"}' $SR/config/orders-value

cat > v1b.json <<'EOF'
{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"namespace\":\"com.example.orders\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"long\"},{\"name\":\"legacyCouponId\",\"type\":\"string\"},{\"name\":\"currency\",\"type\":\"string\",\"default\":\"KRW\"}]}"}
EOF
curl -sX POST -H "$CT" --data @v1b.json \
  "$SR/compatibility/subjects/orders-value/versions/latest?verbose=true"
#   → {"is_compatible":true}

# FORWARD 로 바꾸면 "default 없는 필드 추가"도 통과합니다
curl -sX PUT -H "$CT" --data '{"compatibility":"FORWARD"}' $SR/config/orders-value
cat > v1c.json <<'EOF'
{"schema":"{\"type\":\"record\",\"name\":\"Order\",\"namespace\":\"com.example.orders\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"long\"},{\"name\":\"legacyCouponId\",\"type\":\"string\"},{\"name\":\"channel\",\"type\":\"string\"}]}"}
EOF
curl -sX POST -H "$CT" --data @v1c.json \
  "$SR/compatibility/subjects/orders-value/versions/latest?verbose=true"
#   → {"is_compatible":true}   ★ 같은 변경이 모드에 따라 결과가 다릅니다

# 원래대로 되돌리기 — subject 오버라이드 삭제
curl -sX DELETE $SR/config/orders-value
curl -s $SR/config/orders-value      # → 404 (전역 설정을 따름)

해결

즉시 조치 — 컨슈머를 살리고 프로듀서를 되돌린다

크래시 루프는 같은 오프셋에서 반복해서 죽는 것이므로, 컨슈머를 살리는 방법은 두 가지뿐입니다 — 나쁜 레코드를 건너뛰거나, 나쁜 레코드가 더 생기지 않게 막는 것입니다.

즉시 조치 — 순서를 반드시 이렇게 지킵니다
# 1. 프로듀서를 v1 으로 롤백한다 (가장 먼저. 나쁜 레코드 생성을 멈춥니다)
kubectl rollout undo deploy/order-producer

# 2. 호환성 모드를 되돌린다 (subject 오버라이드 제거 → 전역 BACKWARD 를 따름)
curl -sX DELETE http://schema-registry:8081/config/orders-value

# 3. 잘못 등록된 스키마 버전을 soft delete 한다
#    (hard delete 는 permanent=true. 이미 그 id 로 쓰인 레코드가 있으면 읽을 수 없게 되므로 주의)
curl -sX DELETE http://schema-registry:8081/subjects/orders-value/versions/5

# 4. 컨슈머가 나쁜 레코드를 건너뛰게 한다.
#    예외 메시지가 파티션과 오프셋을 알려 주므로 그 지점 다음으로 seek 합니다.
#    컨슈머를 내린 뒤 오프셋을 앞으로 밀어 줍니다.
kubectl scale deploy/settlement-service --replicas=0

kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 \
  --group order-settlement --topic orders:11 \
  --reset-offsets --shift-by 1 --dry-run     # 확인 후 --execute

kubectl scale deploy/settlement-service --replicas=8

근본 해결 — 변경을 쪼개고, 게이트를 CI에 둔다

비호환 변경 세 개를 한 릴리스에 묶고, 막히자 검사를 껐습니다. 프로듀서가 런타임에 스키마를 등록하므로 배포 전에 검증할 기회도 없었습니다.

변경 전
# Schema Registry — subject 오버라이드
compatibility=NONE

# producer
auto.register.schemas=true
use.latest.version=false

# CI
# 스키마 호환성 검사 단계 없음

모드는 전역 FULL_TRANSITIVE로 강화하고, 프로듀서의 자동 등록을 끄고, 등록·검증을 CI의 명시적 단계로 옮깁니다.

변경 후
# Schema Registry — 전역
compatibility=FULL_TRANSITIVE     # 배포 순서를 고민하지 않아도 되게
# subject 오버라이드 없음

# producer
auto.register.schemas=false        # 런타임 등록 금지
use.latest.version=true

# CI (배포 전 게이트)
# 1) POST /compatibility/subjects/{subject}/versions/latest?verbose=true → is_compatible 확인
# 2) 통과하면 POST /subjects/{subject}/versions 로 등록
# 3) 실패하면 빌드 실패. 모드를 바꿔 우회하는 경로를 아예 막아 둔다
CI 게이트 스크립트 — 이것이 있으면 이 사고는 일어나지 않습니다
#!/usr/bin/env bash
set -euo pipefail
SR="${SCHEMA_REGISTRY_URL}"
CT='Content-Type: application/vnd.schemaregistry.v1+json'

for f in schemas/*.avsc; do
  subject="$(basename "$f" .avsc)-value"
  payload=$(jq -Rs '{schema: .}' < "$f")

  # 1) 호환성 검사 — 등록하지 않고 결과만 받습니다
  res=$(curl -sf -X POST -H "$CT" --data "$payload" \
        "$SR/compatibility/subjects/$subject/versions/latest?verbose=true")
  ok=$(echo "$res" | jq -r '.is_compatible')

  if [ "$ok" != "true" ]; then
    echo "INCOMPATIBLE: $subject"
    echo "$res" | jq -r '.messages[]?'
    echo "→ 호환성 모드를 바꿔 우회하지 마세요. 스키마 변경을 쪼개세요."
    exit 1
  fi

  # 2) 통과했으면 등록
  curl -sf -X POST -H "$CT" --data "$payload" "$SR/subjects/$subject/versions" \
    | jq -r '"registered \($subject) id=\(.id)"'
done

필요한 변경을 여러 릴리스로 쪼개는 방법

이 케이스의 세 가지 변경을 FULL 호환을 유지하면서 달성하는 절차입니다.

비호환 변경을 호환 변경의 연속으로 분해하기
릴리스 스키마 변경 애플리케이션 변경 호환성
R1 amountMinor(string, default "")와 currency(string, default "KRW") 추가. legacyCouponIddefault 부여 프로듀서가 amountamountMinor둘 다 채움 FULL
R2 없음 컨슈머 7개를 순차적으로 amountMinor 읽기로 전환 변경 없음
R3 없음 모든 컨슈머 전환 완료 확인. 대시보드로 검증 변경 없음
R4 amountlegacyCouponId 제거(둘 다 default가 있는 상태) 프로듀서가 amountMinor만 채움 FULL

예방 체크리스트

시험 포인트

공식 문서 출처

Kafka 쪽 사실은 Apache Kafka 4.3 문서·소스에서, Schema Registry 쪽 사실은 Confluent 공식 문서와 confluentinc/schema-registry 소스에서 확인했습니다.