스키마 진화의 필요성, Confluent Schema Registry, Avro/Protobuf/JSON Schema 비교, 호환성 규칙, 데이터 계약 개념까지 스키마 관리 전략을 체계적으로 학습합니다.
실시간 파이프라인에서 프로듀서와 컨슈머는 독립적으로 배포됩니다. 프로듀서가 새 필드를 추가하거나 필드 타입을 변경했는데, 컨슈머가 아직 업데이트되지 않았다면 어떻게 될까요?
JSON을 스키마 없이 사용하는 경우를 생각해 보겠습니다.
{"orderId": "O-001", "amount": 50000, "currency": "KRW"}어느 날 프로듀서 팀이 필드를 추가합니다.
{"orderId": "O-002", "amount": 50000, "currency": "KRW", "discount": 5000}이 정도는 문제가 없습니다. 그런데 다음과 같은 변경이라면?
{"order_id": "O-003", "total": 50000, "curr": "KRW"}필드 이름이 변경되어 모든 컨슈머가 즉시 깨집니다. 스키마 관리 없이는 이런 파괴적 변경을 사전에 차단할 방법이 없습니다.
Confluent Schema Registry는 Kafka 생태계에서 가장 널리 사용되는 스키마 관리 시스템입니다. 스키마를 중앙에서 저장, 버전 관리, 호환성 검증하는 서비스입니다.
동작 흐름은 다음과 같습니다.
# 스키마 등록
curl -X POST http://schema-registry:8081/subjects/orders-value/versions \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{
"schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"currency\",\"type\":\"string\"}]}"
}'
# 최신 스키마 조회
curl http://schema-registry:8081/subjects/orders-value/versions/latest
# 특정 버전 조회
curl http://schema-registry:8081/subjects/orders-value/versions/1
# 모든 서브젝트 목록
curl http://schema-registry:8081/subjects
# 호환성 검사 (등록 전 미리 테스트)
curl -X POST http://schema-registry:8081/compatibility/subjects/orders-value/versions/latest \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"schema": "..."}'스키마는 Subject(서브젝트) 단위로 관리됩니다. 기본 전략인 TopicNameStrategy에서는 [토픽명]-key와 [토픽명]-value가 서브젝트가 됩니다. 예를 들어 orders 토픽의 값 스키마는 orders-value 서브젝트에 저장됩니다.
Schema Registry는 세 가지 주요 직렬화 포맷을 지원합니다.
Avro는 Kafka 생태계에서 가장 오랫동안 사용된 직렬화 포맷입니다.
{
"type": "record",
"name": "Order",
"namespace": "com.shop.events",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "currency", "type": "string", "default": "KRW"},
{"name": "discount", "type": ["null", "double"], "default": null},
{"name": "metadata", "type": {
"type": "map",
"values": "string"
}, "default": {}}
]
}Avro의 특성은 다음과 같습니다.
Google이 개발한 직렬화 포맷으로, 강타입과 성능이 강점입니다.
syntax = "proto3";
package shop.events;
message Order {
string order_id = 1;
double amount = 2;
string currency = 3;
optional double discount = 4;
map<string, string> metadata = 5;
}Protobuf의 특성은 다음과 같습니다.
.proto 파일로 다양한 언어의 클라이언트 코드 자동 생성JSON 데이터의 구조를 정의하는 표준입니다.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"orderId": {"type": "string"},
"amount": {"type": "number"},
"currency": {"type": "string", "default": "KRW"},
"discount": {"type": "number"}
},
"required": ["orderId", "amount"]
}JSON Schema의 특성은 다음과 같습니다.
| 항목 | Avro | Protobuf | JSON Schema |
|---|---|---|---|
| 크기 | 작음 | 매우 작음 | 큼 |
| 속도 | 빠름 | 매우 빠름 | 느림 |
| 가독성 | 바이너리 | 바이너리 | 텍스트 |
| 코드 생성 | 선택적 | 필수 | 불필요 |
| 스키마 진화 | 우수 | 우수 | 보통 |
| Kafka 생태계 | 최적 | 좋음 | 좋음 |
Kafka 생태계에서는 Avro가 가장 널리 사용됩니다. gRPC를 함께 사용하거나 다국어 환경이라면 Protobuf를 고려하세요. JSON Schema는 디버깅 편의성이 중요하고 성능 요구가 낮은 경우에 적합합니다.
Schema Registry의 핵심 기능은 스키마 변경의 호환성을 자동으로 검증하는 것입니다. 새 스키마를 등록할 때, 이전 버전과의 호환성을 확인하여 비호환 변경을 차단합니다.
새 스키마로 이전 데이터를 읽을 수 있음을 보장합니다. 컨슈머가 먼저 업그레이드되는 시나리오에 적합합니다.
허용되는 변경:
v1: [orderId, amount, currency]
v2: [orderId, amount, currency, discount(default=0)] -- 기본값 있는 필드 추가: 호환
v2로 v1 데이터를 읽으면 discount에 기본값 0 적용이전 스키마로 새 데이터를 읽을 수 있음을 보장합니다. 프로듀서가 먼저 업그레이드되는 시나리오에 적합합니다.
허용되는 변경:
v1: [orderId, amount, currency]
v2: [orderId, amount] -- currency 필드 삭제
v1 스키마로 v2 데이터를 읽으면 currency가 없어도 기본값으로 처리BACKWARD + FORWARD. 양방향 호환성을 모두 보장합니다. 가장 안전하지만 변경 범위가 제한됩니다.
허용되는 변경:
호환성 검사를 하지 않습니다. 어떤 변경이든 허용되지만, 프로듀서와 컨슈머 사이의 호환성이 보장되지 않습니다.
# 글로벌 호환성 수준 설정
curl -X PUT http://schema-registry:8081/config \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"compatibility": "BACKWARD"}'
# 서브젝트별 호환성 수준 설정 (글로벌 설정 오버라이드)
curl -X PUT http://schema-registry:8081/config/orders-value \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"compatibility": "FULL"}'호환성 수준을 NONE으로 설정하면 스키마 레지스트리의 핵심 가치를 무력화합니다. 개발 환경에서 빠른 반복을 위해 일시적으로만 사용하고, 프로덕션에서는 최소 BACKWARD를 유지하세요.
스키마는 데이터의 구조(필드 이름, 타입)만 정의합니다. 그러나 실제 운영에서는 구조 외에도 많은 것이 중요합니다.
**Data Contract(데이터 계약)**은 프로듀서와 컨슈머 사이의 포괄적인 합의입니다. 스키마를 포함하되, 데이터 품질 규칙, SLA, 소유권, 변경 프로세스까지 아우릅니다.
# 데이터 계약 예시
contract:
name: "orders-stream"
version: "2.1.0"
owner: "checkout-team"
description: "결제 완료된 주문 이벤트 스트림"
schema:
type: "avro"
subject: "orders-value"
compatibility: "FULL"
quality:
rules:
- field: "amount"
rule: "amount > 0"
severity: "error"
- field: "currency"
rule: "currency IN ('KRW', 'USD', 'EUR', 'JPY')"
severity: "error"
- field: "userId"
rule: "userId IS NOT NULL AND length(userId) > 0"
severity: "error"
sla:
availability: "99.9%"
freshness: "< 30 seconds"
throughput: "> 1000 events/second"
governance:
change_process: "PR to schema repo, review by data-platform team"
breaking_change_notice: "14 days"
deprecation_notice: "90 days"데이터 계약을 자동으로 검증하는 파이프라인을 구축할 수 있습니다.
데이터 계약은 조직 규모가 커질수록 중요해집니다. 소규모 팀에서는 비공식적 소통으로 충분하지만, 수십 개 팀이 수백 개 토픽을 공유하는 환경에서는 명시적 계약 없이는 관리가 불가능합니다.
프로듀서와 컨슈머에서 Schema Registry를 실제로 사용하는 코드를 살펴보겠습니다.
Properties props = new Properties();
props.put("bootstrap.servers", "kafka:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("schema.registry.url", "http://schema-registry:8081");
props.put("auto.register.schemas", "false"); // 프로덕션에서는 자동 등록 비활성화
KafkaProducer<String, GenericRecord> producer = new KafkaProducer<>(props);
// Avro 레코드 생성
Schema schema = schemaRegistry.getLatestSchema("orders-value");
GenericRecord record = new GenericData.Record(schema);
record.put("orderId", "O-001");
record.put("amount", 50000.0);
record.put("currency", "KRW");
producer.send(new ProducerRecord<>("orders", "O-001", record));Properties props = new Properties();
props.put("bootstrap.servers", "kafka:9092");
props.put("group.id", "order-processor");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "io.confluent.kafka.serializers.KafkaAvroDeserializer");
props.put("schema.registry.url", "http://schema-registry:8081");
props.put("specific.avro.reader", "false"); // GenericRecord 사용
KafkaConsumer<String, GenericRecord> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("orders"));
while (true) {
ConsumerRecords<String, GenericRecord> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, GenericRecord> record : records) {
GenericRecord order = record.value();
String orderId = order.get("orderId").toString();
double amount = (double) order.get("amount");
// discount 필드가 없는 v1 메시지도 안전하게 처리
Object discount = order.get("discount"); // null 가능
}
}프로덕션에서는 auto.register.schemas=false를 설정하여 프로듀서가 임의로 새 스키마를 등록하지 못하도록 해야 합니다. 스키마 등록은 CI/CD 파이프라인을 통해서만 허용하세요.
이번 장에서는 스키마 레지스트리와 데이터 계약을 학습했습니다.
10장에서는 Exactly-once 보장과 신뢰성을 다룹니다. 세 가지 전달 보장 수준의 구현 원리, Kafka 트랜잭션과 Flink 체크포인트의 조합, 멱등성 설계, DLQ, 백프레셔, 장애 복구 전략까지 프로덕션 수준의 신뢰성을 확보하는 방법을 학습합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
At-least-once/At-most-once/Exactly-once 비교, Kafka 트랜잭션과 Flink 체크포인트의 조합, 멱등성 설계, DLQ, 백프레셔, 장애 복구 전략까지 프로덕션 신뢰성을 학습합니다.
CDC의 원리와 WAL 기반 변경 캡처, Debezium 아키텍처, PostgreSQL/MySQL CDC 실습, Flink CDC 3.6, 아웃박스 패턴, 이벤추얼 컨시스턴시까지 데이터 통합의 핵심을 학습합니다.
브로커/프로듀서/컨슈머 핵심 메트릭, Prometheus/Grafana 대시보드, 알림 설계, 용량 계획, 비용 최적화, 실전 아키텍처까지 실시간 파이프라인의 프로덕션 운영을 학습합니다.