에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/메시징

[Kafka] 직렬화와 스키마 관리: JSON vs Avro, Schema Registry 호환성 검사

반응형
[Kafka] 직렬화와 스키마 관리: JSON vs Avro, Schema Registry 호환성 검사

[Kafka] 직렬화와 스키마 관리: JSON vs Avro, Schema Registry 호환성 검사

카프카는 메시지를 바이트로 주고받아요. 그래서 객체를 바이트로 바꾸고(직렬화) 다시 객체로 되돌리는(역직렬화) 방식을 정해야 해요. 처음엔 JSON으로 시작하지만 서비스가 커지고 메시지 구조가 바뀌기 시작하면 "스키마를 어떻게 관리하느냐"가 큰 문제로 떠올라요.

직렬화가 무엇인지에서 시작해 JSON 직렬화와 그 한계, Avro와 Schema Registry, 스키마 진화와 호환성, JSON과 Avro의 선택 기준, 자주 만나는 문제 순으로 따라가요. 역직렬화 실패 처리는 에러 핸들링과 DLQ 글과 이어져요.

01. 직렬화란 무엇인가

직렬화는 객체를 바이트로 바꾸는 일이에요. 프로듀서가 보낼 때 직렬화하고 컨슈머가 받을 때 역직렬화해요. 카프카는 key와 value 각각에 Serializer·Deserializer를 지정해요.

여기서 중요한 건, 프로듀서와 컨슈머가 같은 방식·같은 구조를 알아야 한다는 거예요. 프로듀서가 보낸 형식을 컨슈머가 모르면 역직렬화에서 깨져요. 그래서 직렬화는 단순히 "어떻게 바이트로 바꾸냐"가 아니라 "양쪽이 메시지 구조를 어떻게 공유하느냐"의 문제예요. 이게 스키마 이야기로 이어져요.

02. JSON 직렬화, 시작하기 쉬워요

가장 흔한 시작은 JSON이에요. 스프링 카프카의 JsonSerializer·JsonDeserializer를 쓰면 객체를 JSON으로 주고받아요.

spring:
  kafka:
    producer:
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
    properties:
      spring.json.trusted.packages: "com.example.order"

JSON은 사람이 읽기 쉽고, 다루기 편하고, 시작이 빨라요. 작은 서비스나 초기 단계에는 충분해요. 컨슈머가 역직렬화할 타입을 알아야 하니 타입 정보를 헤더에 담거나 trusted.packages로 신뢰 패키지를 지정해요.

03. JSON의 한계, 스키마가 없어요

서비스가 커지면 JSON의 약점이 드러나요. 핵심은 JSON 자체에는 스키마가 없다는 거예요.

JSON과 Avro Schema Registry 비교. JSON은 필드 변경을 아무도 막지 않아 컨슈머 역직렬화에서 런타임 사고로 발견되고, Avro는 Registry가 호환성을 검사해 위험한 변경을 배포 전에 차단한다
JSON 환경의 진짜 함정은 "변경이 막히지 않는다"는 거예요. 프로듀서가 필드 이름이나 타입을 바꿔도 카프카는 바이트를 그대로 전달할 뿐 아무것도 검증하지 않아요. 사고는 그 메시지를 받은 컨슈머의 역직렬화에서, 그것도 배포가 다 끝난 런타임에 터져요. 보내는 팀과 받는 팀이 다르면 "누가 언제 바꿨는지" 추적부터 일이 되고요.

게다가 JSON은 필드 이름을 매 메시지에 다 담으니 용량도 커요. 메시지를 주고받는 서비스가 여럿이고 구조가 계속 진화하는 환경에서는, "구조 변경을 안전하게 관리하는 장치"가 필요해져요. 그게 스키마와 Schema Registry예요.

04. Avro와 Schema Registry

Avro는 스키마를 명시적으로 정의하는 직렬화 포맷이에요. 메시지 구조를 스키마 파일로 먼저 정하고 그 스키마에 맞춰 직렬화해요.

{
  "type": "record",
  "name": "Order",
  "fields": [
    {"name": "id", "type": "string"},
    {"name": "amount", "type": "long"}
  ]
}

여기에 Schema Registry가 붙어요. Schema Registry는 스키마를 중앙에서 관리하는 서버예요.

Schema Registry 동작 구조. 프로듀서가 스키마를 Registry에 등록하고 메시지에는 스키마 ID만 담아 보내면, 컨슈머가 ID로 Registry에서 스키마를 조회해 역직렬화한다

프로듀서가 메시지를 보낼 때 스키마를 Registry에 등록하고 메시지에는 스키마 전체가 아니라 스키마 ID만 담아요. 컨슈머는 그 ID로 Registry에서 스키마를 받아 역직렬화해요. 이 방식의 이점은 두 가지예요. 메시지에 스키마 ID만 담으니 용량이 작아져요. 그리고 스키마가 중앙에서 관리되니, 다음에 볼 호환성 검사를 할 수 있어요.

05. 스키마 진화와 호환성

Schema Registry의 진짜 가치는 호환성 검사예요. 스키마를 바꾸려 할 때, 그 변경이 기존과 호환되는지 Registry가 미리 검사해서 위험한 변경을 막아줘요.

Schema Registry 호환성 모드. backward는 새 스키마로 옛 데이터를 읽을 수 있어야 해서 컨슈머 먼저 배포가 안전하고, forward는 프로듀서 먼저 배포가 안전하며, full은 양방향 호환이라 배포 순서가 자유롭다
  • backward — 새 스키마로 옛 데이터를 읽을 수 있어야 해요. 컨슈머를 먼저 올릴 때 안전해요. 가장 흔히 써요.
  • forward — 옛 스키마로 새 데이터를 읽을 수 있어야 해요. 프로듀서를 먼저 올릴 때 안전해요.
  • full — 양방향 모두 호환. 가장 엄격해요.

예를 들어 backward 호환에서는, 필드를 추가할 때 기본값을 주면 옛 데이터에 그 필드가 없어도 기본값으로 읽혀서 안전해요. 막히는 건 기본값 없는 필드 추가예요. 새 스키마로 옛 데이터를 읽을 때 그 필드를 채울 값이 없으니까요.

직관과 반대인 부분도 있어요. backward에서 필드 삭제는 허용돼요. 옛 데이터의 지워진 필드는 무시하면 되거든요. 필드 삭제가 막히는 건 forward 쪽이고 타입 변경도 int에서 long 같은 승격은 허용돼요. 이렇게 위험한 변경이 스키마 등록 시점에 걸러지니, "컨슈머에서 터지는 사고"가 "등록 단계에서 막히는 검사"로 바뀌어요. 이게 JSON과 가장 큰 차이예요.

06. Avro를 코드에서 쓰는 법

Avro 스키마는 정해놓으면 그걸로 자바 클래스를 생성해서 써요. 빌드 플러그인이 .avsc 스키마에서 Order 같은 클래스를 만들어주고 우리는 그 객체를 보내고 받아요.

spring:
  kafka:
    producer:
      value-serializer: io.confluent.kafka.serializers.KafkaAvroSerializer
    consumer:
      value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
    properties:
      schema.registry.url: http://localhost:8081

여기서 schema.registry.url로 Registry 주소를 알려주면, 직렬화기가 알아서 스키마를 등록하고 ID를 메시지에 붙여요. 컨슈머도 같은 Registry를 보고 스키마를 받아 역직렬화해요. 우리가 스키마 ID를 직접 다룰 일은 없어요.

알아둘 함정 하나, subject 네이밍이에요. Registry는 스키마를 subject 단위로 관리하는데, 기본은 토픽이름-value예요. 호환성 검사도 이 subject 단위로 일어나고요. 그래서 한 토픽에 여러 종류의 메시지를 섞어 보내면 서로 다른 구조가 한 subject에서 충돌해 검사가 꼬여요. 보통 한 토픽엔 한 종류의 메시지를 두는 게 깔끔해요.

07. JSON vs Avro, 무엇을 고르나

둘은 단계와 규모로 갈려요.

  • JSON — 시작이 빠르고 다루기 쉬워요. 서비스가 적고 구조가 안정적이거나 초기 단계면 충분해요. 사람이 읽기 좋아 디버깅도 편해요.
  • Avro + Schema Registry — 구조가 계속 진화하고 여러 서비스가 같은 토픽을 주고받는 환경에 맞아요. 호환성을 강제해 사고를 막고 용량도 작아요. 대신 Registry라는 인프라가 추가돼요.

정리하면, 처음엔 JSON으로 빠르게 시작해요. 메시지를 공유하는 서비스가 늘고 구조 변경이 잦아지면 Avro + Schema Registry로 옮기는 흐름이 자연스러워요. 핵심 판단 기준은 "스키마 변경을 사람이 조심해서 관리할 수 있는 규모인가, 아니면 도구로 강제해야 하는 규모인가"예요.

08. 자주 만나는 문제

컨슈머에서 역직렬화가 터져요

프로듀서가 보낸 구조와 컨슈머가 기대하는 구조가 다른 거예요. JSON이면 필드·타입 변경이 원인일 때가 많아요. ErrorHandlingDeserializer로 감싸 사고를 흡수하고(에러 핸들링 참고), 근본적으로는 스키마 관리로 넘어가는 걸 검토해요.

스키마를 바꿨더니 배포가 막혀요

Schema Registry의 호환성 검사에 걸린 거예요. 막힌 거지 버그가 아니에요. 호환을 깨는 변경(필드 삭제·타입 변경)을 하려는 거니, 기본값을 주는 추가로 바꾸거나 호환성 전략을 다시 검토해요.

메시지 용량이 너무 커요

JSON이 필드 이름을 매번 담아 그래요. 대량 트래픽이면 Avro로 옮겨 스키마 ID만 담게 하면 용량이 크게 줄어요.

직렬화, 규모로 갈려요

직렬화는 객체를 바이트로 바꾸는 동시에 프로듀서·컨슈머가 메시지 구조를 공유하는 일이에요. JSON은 시작이 쉽지만 스키마가 없어 변경을 런타임에 사고로 발견해요. Avro + Schema Registry는 스키마를 중앙에서 관리하고 호환성을 강제해 사고를 배포 전에 막고 용량도 줄여요. 규모가 작으면 JSON, 여러 서비스가 진화하는 구조를 공유하면 Avro로 가는 게 기준이에요.

여기까지가 @KafkaListener 설정에서 lag 모니터링까지 이어 온 그림의 마지막 조각이에요. 컨슈머·프로듀서·운영·테스트·직렬화를 함께 두고 보면 카프카를 실무에 올리는 전체 그림이 손에 잡혀요.

출처: Confluent — Schema Registry · Apache Avro Documentation · Spring for Apache Kafka — Serialization

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."