← 목록으로
AI·데이터
#스키마 레지스트리#스키마 진화#호환성#Avro#이벤트 기반 아키텍처
최종 업데이트 · 2026-10-25

스키마 레지스트리(Schema Registry)와 스키마 진화

1. 개요

스키마 레지스트리(Schema Registry) 란 이벤트 스트림·메시지의 데이터 구조(스키마)를 중앙에서 등록·버전관리하고, 생산자(Producer)와 소비자(Consumer)가 주고받는 메시지가 합의된 스키마를 따르는지 검증하며, 스키마 변경 시 호환성(compatibility) 규칙에 따라 안전한 진화를 강제하는 메타데이터 관리 시스템이다.

Apache Kafka 같은 이벤트 기반 아키텍처(EDA)가 확산되면서, 하나의 토픽을 수십 개의 서비스가 동시에 생산·소비하는 구조가 보편화되었다. 그러나 메시지 본문은 바이트 배열에 불과하므로, 생산자가 필드를 추가하거나 타입을 바꾸면 이를 모르는 소비자는 역직렬화(deserialization)에 실패하거나 조용히 잘못된 값을 읽는다. 이처럼 서로 다른 시점에 배포된 서비스들이 암묵적으로 같은 데이터 구조를 가정하다가 어긋나는 현상을 스키마 드리프트(schema drift) 라 하며, 분산 시스템의 대표적 운영 장애 원인이다.

전통적 방식은 생산자와 소비자가 공통 라이브러리에 스키마를 하드코딩하여 공유하는 것이었다. 그러나 이는 두 가지 근본 문제를 낳는다. 첫째, 스키마를 바꿀 때마다 모든 서비스를 동시에 재배포해야 하여 느슨한 결합(loose coupling)이라는 EDA의 본질이 깨진다. 둘째, 메시지마다 스키마 전체를 함께 전송하면 페이로드가 비대해진다. 스키마 레지스트리는 스키마를 한 번 등록하고 정수 ID로 참조하게 하여 이 두 문제를 동시에 해결한다. 메시지에는 스키마 ID만 5바이트가량 실리고, 실제 구조 정의는 레지스트리에서 조회한다.

스키마 레지스트리의 핵심 가치는 배포 순서의 독립성이다. 생산자와 소비자가 서로 다른 시점에, 서로 모른 채 배포되어도 메시지 호환성이 깨지지 않도록 "변경 가능한 것"과 "변경 불가능한 것"을 사전에 규약으로 못 박는다. 이는 단순한 유효성 검사기를 넘어, 데이터를 하나의 계약(contract) 으로 다루는 거버넌스 도구로 발전하고 있다.

정리하면 스키마 레지스트리는 다음 네 가지 특징을 갖는다.

  • 중앙 집중 등록·버전관리: 스키마를 서브젝트 단위로 등록하고 버전·전역 ID를 부여한다.
  • 호환성 강제: 등록 시점에 정책을 검사해 깨지는 변경을 사전 차단한다.
  • 참조 효율: 메시지에는 스키마 ID만 실어 페이로드를 최소화한다.
  • 느슨한 결합: 생산자·소비자의 독립 배포를 가능하게 한다.

이러한 문제의식은 동기식 REST API에서의 API 버저닝과 본질이 같다. REST에서는 /v1, /v2처럼 URI로 버전을 명시하고 클라이언트가 원하는 버전을 선택하지만, 비동기 이벤트 스트림에서는 소비자가 메시지를 "선택"할 수 없고 토픽에 쌓인 모든 과거·현재 메시지를 순서대로 받아야 한다. 따라서 이벤트 세계의 버전 관리는 "클라이언트가 고르는" 방식이 아니라 "어떤 버전이든 깨지지 않게 읽히는" 호환성 보장으로 풀어야 하며, 바로 이 역할을 스키마 레지스트리가 담당한다. 즉 스키마 레지스트리는 이벤트 기반 아키텍처를 위한 API 계약 관리 계층으로 이해하는 것이 정확하다.

2. 스키마 레지스트리의 구조와 동작

스키마 레지스트리는 생산자·소비자·직렬화기(SerDes)·저장소로 구성된 조회형 서비스다. 생산자는 메시지를 보내기 전에 스키마를 등록하고 ID를 받아 메시지 앞에 붙이며, 소비자는 그 ID로 스키마를 역조회하여 안전하게 해석한다. 아래 구조도는 전체 구성 요소의 관계를 나타낸다.

graph LR
  subgraph P["생산자 측"]
    PA["애플리케이션"] --> PS["직렬화기(SerDes)"]
  end
  subgraph R["스키마 레지스트리"]
    API["REST API"] --> STORE["스키마 저장소(Kafka 토픽/DB)"]
    API --> CHK["호환성 검사기"]
  end
  subgraph C["소비자 측"]
    CS["역직렬화기(SerDes)"] --> CA["애플리케이션"]
  end
  PS -->|"스키마 등록/조회"| API
  PS -->|"ID+페이로드"| BROKER["메시지 브로커(Kafka)"]
  BROKER -->|"ID+페이로드"| CS
  CS -->|"ID로 스키마 조회"| API

직렬화기(SerDes) 는 레지스트리와 통신하는 클라이언트 라이브러리로, 가장 중요한 요소다. 생산자 측 직렬화기는 전송 직전 객체의 스키마를 레지스트리에 등록(또는 캐시에서 ID 확인)하고, 메시지 앞에 "매직 바이트 + 4바이트 스키마 ID"를 붙인 뒤 본문을 이진 인코딩한다. 소비자 측은 그 ID를 떼어내 레지스트리에서 스키마를 받아(한 번 받으면 로컬 캐시) 역직렬화한다. 덕분에 네트워크에는 스키마 ID만 오가고, 레지스트리 조회는 캐시 미스 때만 발생하여 처리량에 거의 영향을 주지 않는다.

스키마 저장소는 등록된 스키마와 버전, 서브젝트(subject) 메타데이터를 보관한다. Confluent 구현은 별도 DB 대신 _schemas라는 Kafka 내부 토픽을 로그로 사용하여 레지스트리 인스턴스들이 이를 재생(replay)해 동일 상태를 복원한다. 이로써 레지스트리 자체가 Kafka의 복제·내구성을 그대로 물려받는다. 서브젝트는 스키마 버전들의 이름공간으로, 보통 <토픽명>-value, <토픽명>-key 형태를 쓴다(TopicNameStrategy). 하나의 서브젝트가 v1, v2, v3처럼 여러 버전을 가지며, 각 버전은 전역 고유 ID를 부여받는다.

호환성 검사기는 새 스키마를 등록하려 할 때 기존 버전과 비교하여 설정된 호환성 정책을 위반하면 등록을 거부한다. 예컨대 어떤 소비자가 필수로 읽는 필드를 생산자가 삭제하려 하면, 레지스트리는 등록 단계에서 이를 막아 장애를 배포 전에 차단한다. 이 "등록 시점 게이트"가 스키마 레지스트리를 단순 저장소가 아닌 거버넌스 통제점으로 만든다. 실제로 금융권의 실시간 거래 파이프라인에서는 CI 파이프라인에 호환성 검사를 포함시켜, 호환되지 않는 스키마 변경 PR을 빌드 단계에서 실패시키는 방식(이른바 schema-as-code)을 쓴다.

메시지 전송 시의 바이트 레이아웃을 구체적으로 보면 그 효율을 체감할 수 있다. Confluent 포맷의 메시지는 "매직 바이트(0x00) 1바이트 + 스키마 ID 4바이트 + 실제 페이로드"로 구성된다. 즉 스키마 정의 전체(수백 바이트에서 수 KB에 이를 수 있는 JSON/Avro 스키마)를 매 메시지마다 싣는 대신, 5바이트의 접두사만으로 어떤 구조인지 식별한다. 초당 수십만 건을 흘려보내는 토픽에서 이 차이는 네트워크 대역폭과 저장 비용을 수십 배 절감한다. 소비자는 처음 보는 ID일 때만 레지스트리를 1회 조회하고 이후에는 로컬 캐시를 사용하므로, 레지스트리는 트래픽 경로가 아니라 제어 평면(control plane) 에만 위치한다. 이 설계가 "중앙 집중 거버넌스"와 "분산 고처리량"을 동시에 달성하는 핵심이다.

3. 스키마 진화와 호환성 정책

스키마 진화의 핵심 질문은 "스키마가 바뀔 때 누가 먼저 배포되어야 안전한가"이다. 호환성 정책은 이 배포 순서를 규정한다. 아래 상태 흐름은 호환성 검사 과정을 보여준다.

flowchart TD
  A["새 스키마 등록 요청"] --> B{"호환성 모드 확인"}
  B -->|"NONE"| G["검사 생략 후 등록"]
  B -->|"BACKWARD"| C["새 스키마로 이전 버전 데이터 읽기 가능?"]
  B -->|"FORWARD"| D["이전 스키마로 새 버전 데이터 읽기 가능?"]
  B -->|"FULL"| E["양방향 모두 가능?"]
  C -->|"가능"| G
  C -->|"불가"| F["등록 거부(409)"]
  D -->|"가능"| G
  D -->|"불가"| F
  E -->|"가능"| G
  E -->|"불가"| F
  G --> H["새 버전 번호와 전역 ID 부여"]

Confluent Schema Registry는 일곱 가지 호환성 모드(BACKWARD, BACKWARD_TRANSITIVE, FORWARD, FORWARD_TRANSITIVE, FULL, FULL_TRANSITIVE, NONE)를 제공한다. 기본값은 BACKWARD 이다. _TRANSITIVE 접미사가 붙으면 검사 대상이 "직전 버전 하나"에서 "지금까지 등록된 모든 버전"으로 확장되어, 과거 어떤 버전으로 쓰인 데이터든 안전하게 다룰 수 있다.

모드 허용되는 변경(대표) 검사 기준 업그레이드 순서
BACKWARD(기본) 필드 삭제, 기본값 있는 필드 추가 직전 버전 소비자 먼저
BACKWARD_TRANSITIVE 위와 동일 모든 버전 소비자 먼저
FORWARD 필드 추가, 기본값 있는 필드 삭제 직전 버전 생산자 먼저
FORWARD_TRANSITIVE 위와 동일 모든 버전 생산자 먼저
FULL 기본값 있는 필드 추가·삭제 직전 버전 임의 순서
FULL_TRANSITIVE 위와 동일 모든 버전 임의 순서
NONE 모든 변경 검사 안 함 보장 없음

후방 호환(BACKWARD) 은 "새 스키마로 옛 데이터를 읽을 수 있어야 한다"는 규칙이다. 따라서 소비자를 먼저 신 버전으로 올린 뒤 생산자를 올린다. 필드를 삭제하거나 기본값이 있는 필드를 추가하는 변경이 허용된다. Confluent이 이를 기본값으로 삼는 이유는, 토픽을 처음부터 되감아(rewind) 재처리할 때도 최신 소비자가 과거 메시지를 모두 읽을 수 있기 때문이다. 로그 재처리·장애 복구가 잦은 실무에서 가장 안전한 선택이다.

전방 호환(FORWARD) 은 반대로 "옛 스키마로 새 데이터를 읽을 수 있어야 한다"는 규칙이며, 생산자를 먼저 올린다. 아직 구 버전인 소비자가 신 버전 생산자가 만든 메시지를 깨지 않고 읽어야 하는 상황, 예컨대 외부 파트너가 소비자를 통제하여 즉시 업그레이드할 수 없는 B2B 연동에 적합하다. 완전 호환(FULL) 은 양방향을 모두 요구하여 배포 순서 자체를 자유롭게 하지만, 허용되는 변경 폭이 "기본값이 있는 필드의 추가·삭제"로 좁아진다. 통제가 가장 어려운 대규모 조직에서 안전을 극대화할 때 쓴다.

호환성 검사를 통과하지 못하는 변경, 즉 깨지는 변경(breaking change) 이 꼭 필요할 때도 있다. 이때는 같은 서브젝트를 억지로 진화시키는 대신 새 토픽(예: orders.v2)을 만들어 신·구 스트림을 병행 운영하고, 소비자를 점진적으로 이전시키는 이중 쓰기(dual-write)·확장 후 수축(expand-and-contract) 패턴을 쓴다. 생산자가 한동안 양쪽 토픽에 모두 쓰고, 모든 소비자가 신 토픽으로 넘어간 뒤 구 토픽을 폐기하는 식이다. 스키마 레지스트리는 이 전환 기간 동안에도 각 토픽의 호환성을 독립적으로 보장하여, 대규모 마이그레이션을 무중단으로 수행할 수 있게 한다.

실무 함의는 분명하다. 호환성 모드 선택은 기술적 선호가 아니라 조직의 배포 거버넌스 결정이다. 생산자·소비자를 같은 팀이 통제하면 BACKWARD로 충분하지만, 소비자가 여러 조직에 흩어져 통제 불가능하면 FULL_TRANSITIVE로 올려 "깨지지 않음"을 강하게 보장해야 한다. 한편 NONE은 검사를 끄므로 개발 초기 외에는 금물이며, 운영계에서 NONE을 남발하면 레지스트리는 단지 "값비싼 버전 저장소"로 전락한다.

4. 직렬화 포맷 비교(Avro·Protobuf·JSON Schema)

스키마 레지스트리는 특정 직렬화 포맷에 종속되지 않지만, 각 포맷은 스키마 진화를 다루는 방식이 다르다. Confluent 5.5 이후 세 포맷을 모두 지원하며, 포맷마다 "무엇이 호환 변경인지"의 규칙이 다르다. 예컨대 FORWARD 모드에서 Protobuf는 새 메시지 타입 추가가 금지되는 등 포맷별 제약이 존재한다.

구분 Apache Avro Protobuf JSON Schema
인코딩 이진(스키마 분리) 이진(태그 번호) 텍스트(JSON)
진화 식별 필드 이름 + 기본값 필드 번호 속성 이름
크기/속도 작고 빠름 작고 빠름 크고 느림
가독성 낮음 낮음 높음
적합 영역 데이터 파이프라인·분석 gRPC·고성능 RPC 웹/REST 친화

Avro 는 데이터가 스키마와 분리되어 전송되고 역직렬화 시 "쓰기 스키마(writer)"와 "읽기 스키마(reader)"를 함께 사용해 필드를 이름으로 매핑한다. 이 구조 덕분에 스키마 진화 규칙이 명확하고, 빅데이터 저장(Hadoop·데이터 레이크)의 사실상 표준으로 자리 잡았다. 단, 기본값(default) 설계가 진화의 핵심이어서, 추가 필드에 기본값을 빠뜨리면 후방 호환이 깨진다.

예를 들어 Avro에서 주문 이벤트에 coupon_code라는 선택 필드를 추가한다고 하자. 후방 호환(BACKWARD)을 지키려면 이 필드에 반드시 기본값(예: "default": "")을 지정해야 한다. 그래야 신 버전 소비자가 이 필드가 없는 과거 메시지를 읽을 때 기본값으로 채워 역직렬화에 성공한다. 만약 기본값을 빠뜨리면 레지스트리는 등록 자체를 거부(HTTP 409)하여, 운영에 배포되기 전에 설계 실수를 드러낸다. 이처럼 "기본값 설계"는 Avro 진화의 가장 흔한 실수 지점이자 호환성의 핵심 장치다.

Protobuf 는 필드를 이름이 아닌 정수 태그 번호로 식별한다. 따라서 번호만 유지하면 이름을 바꿔도 호환되며, gRPC와 결합해 낮은 지연의 서비스 간 통신에 강하다. 다만 한 번 쓴 태그 번호를 재사용하면 데이터가 조용히 오염되므로, 삭제한 번호를 reserved로 영구 봉인하는 규율이 필수다. JSON Schema 는 텍스트 기반이라 사람이 읽기 쉽고 웹 생태계와 친화적이지만, 페이로드가 크고 파싱이 느려 초고처리량 스트리밍에는 부적합하다. 실무에서는 "대량 분석 파이프라인은 Avro, 내부 마이크로서비스 RPC는 Protobuf, 외부 공개 API 이벤트는 JSON Schema"처럼 영역별로 혼용하는 것이 일반적이다.

5. 구현 사례 비교

대표 구현으로는 Confluent Schema Registry(Kafka 생태계 사실상 표준), AWS Glue Schema Registry(관리형, Kinesis·MSK 연동), 오픈소스 Apicurio Registry(Avro·Protobuf·JSON Schema·OpenAPI·AsyncAPI까지 폭넓게 지원)가 있다. Confluent은 _schemas 토픽 기반으로 Kafka에 깊이 통합되어 운영 성숙도가 높고, Glue는 서버리스로 운영 부담이 적으며 IAM과 자연스럽게 결합된다. Apicurio는 벤더 종속을 피하려는 조직이나 API 스펙까지 함께 관리하려는 경우에 적합하다.

규모의 예를 들면, 넷플릭스·링크드인급 조직은 하루 수조 건의 이벤트를 처리하며 수천 개의 서브젝트를 운영한다. 이 규모에서는 스키마 ID 캐싱, 레지스트리 다중화, 그리고 스키마 변경을 코드 리뷰처럼 다루는 승인 워크플로가 없으면 사소한 필드 변경 하나가 전사 파이프라인을 멈출 수 있다. 실제로 한 핀테크 사례에서는 결제 이벤트 스키마에 호환성 검사를 CI에 통합한 뒤 스키마 관련 운영 장애가 분기당 수 건에서 0에 가깝게 감소했다고 보고된다(일반적 경향으로 이해).

선택 기준을 정리하면 다음과 같다. 세 구현은 호환성 모델이라는 본질은 공유하지만, 운영 책임의 위치와 지원 범위에서 갈린다.

구분 Confluent AWS Glue Apicurio
저장 방식 Kafka _schemas 토픽 관리형(서버리스) SQL DB/Kafka 등 선택
운영 부담 자체 운영(셀프 호스팅) 거의 없음(완전관리) 자체 운영
지원 스펙 Avro·Protobuf·JSON Schema Avro·Protobuf·JSON 위 + OpenAPI·AsyncAPI
적합 상황 Kafka 중심·고성숙 운영 AWS 종속·운영 간소화 멀티 스펙·벤더 중립

서브젝트 네이밍 전략도 실무에서 중요한 설계 결정이다. 기본인 TopicNameStrategy(<토픽>-value)는 한 토픽에 한 종류의 이벤트만 담길 때 단순하지만, 하나의 토픽에 여러 이벤트 타입을 섞어 순서를 보존해야 하는 경우에는 RecordNameStrategy(레코드 타입명 기준)나 TopicRecordNameStrategy(토픽+레코드명)로 바꿔야 한다. 전략을 잘못 고르면 서로 다른 이벤트가 같은 서브젝트를 공유하며 호환성 검사가 엉뚱하게 작동하므로, 토픽 설계 초기에 결정해 두어야 한다.

6. 심화: 데이터 계약(Data Contract)으로의 진화

최근 스키마 레지스트리는 데이터 계약(Data Contract) 과 데이터 메시(Data Mesh) 논의의 중심 기술로 재평가되고 있다. 데이터 계약은 "데이터를 생산하는 팀과 소비하는 팀 사이의 명시적·버전관리되는 약속"으로, 스키마(구조)뿐 아니라 의미(semantics), 품질 기대치(SLO), 소유권까지 포함한다. 스키마 레지스트리는 이 계약의 기계가 강제할 수 있는 구조 부분을 담당한다. 데이터 메시에서 각 도메인이 데이터를 "제품(data product)"으로 공개할 때, 호환성 정책은 곧 제품의 API 안정성 등급과 같다.

또 하나의 성숙 신호는 스키마 참조(schema references) 다. 공통 타입(예: Address, Money)을 별도 스키마로 정의하고 여러 이벤트 스키마가 이를 참조하도록 하여, 조직 전체의 데이터 정의를 재사용·표준화한다. 이는 마이크로서비스 간 "공유 커널(shared kernel)"을 코드 라이브러리가 아니라 레지스트리에서 선언적으로 관리하는 셈이며, 중복 정의로 인한 의미 불일치를 줄인다. 다만 참조가 깊어지면 변경 영향 범위가 넓어지므로, 공통 스키마에는 더 엄격한 호환성 정책(FULL_TRANSITIVE)과 변경 승인 절차를 적용하는 계층적 거버넌스가 필요하다.

Confluent은 이에 대응해 Data Contracts 기능으로 스키마에 메타데이터·태그(예: PII 표시), 규칙(rule) 기반 검증·변환, 마이그레이션 규칙을 결합하는 방향으로 확장하고 있다. 예컨대 개인정보(PII) 필드를 스키마 태그로 표시하면, 하류 파이프라인에서 자동으로 마스킹·암호화 규칙을 적용하도록 연결할 수 있다. 이는 스키마 레지스트리가 단순 직렬화 보조 도구에서 데이터 거버넌스·개인정보보호의 실행 지점으로 승격되고 있음을 보여준다. 향후에는 스키마 레지스트리가 데이터 카탈로그·리니지(계보) 시스템과 통합되어, 스키마 변경이 하류에 미치는 영향을 자동 분석(impact analysis)하는 방향으로 발전할 전망이다.

7. 고려사항 및 시사점

  • 호환성 정책은 조직 설계 문제다. 기술사 관점에서 BACKWARD/FORWARD/FULL의 선택은 성능이 아니라 "생산자와 소비자를 누가 통제하는가"라는 조직 구조에서 역산해야 한다. 내부 단일 팀은 BACKWARD, 다조직·외부 연동은 FULL_TRANSITIVE가 기본 전략이며, 정책을 서브젝트별로 차등 적용하는 설계가 바람직하다.
  • 스키마를 코드처럼 관리(schema-as-code)하라. 스키마 변경을 Git PR과 CI 호환성 검사로 다뤄 배포 전에 깨짐을 차단해야 한다. 운영계에서 NONE 모드를 허용하는 순간 레지스트리의 거버넌스 가치가 사라지므로, NONE은 조직 정책으로 금지하고 예외는 승인 절차를 둔다.
  • 포맷 선택의 트레이드오프를 명확히 하라. Avro(분석·파이프라인), Protobuf(저지연 RPC), JSON Schema(외부 공개 API)는 각각 진화 규칙과 성능 특성이 다르다. Protobuf의 태그 번호 재사용 금지, Avro의 기본값 설계 같은 포맷별 함정을 가이드라인으로 문서화해야 운영 사고를 예방한다.
  • 단일 장애점(SPOF)과 가용성. 직렬화기가 레지스트리에 의존하므로, 레지스트리 장애가 전체 생산·소비를 멈출 수 있다. 다중화·로컬 스키마 캐시·읽기 전용 폴백을 설계하고, 레지스트리의 가용성 SLO를 메시지 브로커 수준으로 맞춰야 한다.
  • 거버넌스·개인정보 연계 전망. 스키마 태그 기반 PII 식별, 데이터 계약, 리니지·카탈로그 통합은 스키마 레지스트리를 데이터 거버넌스의 실행 지점으로 확장한다. 향후 데이터 메시 환경에서 각 도메인 데이터 제품의 품질·호환성·개인정보 통제를 자동화하는 핵심 축으로 자리 잡을 것이다.

참고자료


한 줄 요약: 스키마 레지스트리는 이벤트 스트림의 데이터 구조를 중앙에서 버전관리하고 호환성 정책(BACKWARD·FORWARD·FULL)으로 안전한 스키마 진화를 강제하여, 생산자·소비자를 느슨하게 결합하고 데이터 계약·거버넌스의 실행 지점으로 발전하는 기술이다.