Big Data/Kafka

[Kafka] Avro Consumer의 GenericRecord schema

devidea 2021. 12. 30. 01:44

이번 글에서는 KafkaAvroDeserializer를 사용한 컨슈머에 대해서 이야기하려고 한다. KafkaAvroDeserializer를 사용했다는 것은 Schema-Registry(이하 스키마 레지스트리)를 사용했다는 의미이다.  스키마 레지스트리를 간단히 설명하면 Confluent에서 제공한 Schema 관리 시스템이다. 카프카로 들어오는 레코드들의 버전 관리가 필요할 때 스키마 레지스트리를 사용한다.

도입부에 많은 용어들을 나열했는데 기본적인 내용을 먼저 정리해야 이 글에서 이야기할 내용이 전달될 수 있을 듯 하다. 
다음 용어들의 개념을 간단히만 살펴보고(자세한 내용은 링크 추가) 이해를 돕고자 한다.

  • AVRO(이하 에이브로)
  • 스키마
  • 스키마 레지스트리

1. 에이브로
에이브로는 스키마, 스키마 레지스트리를 설명하기 전에 알아야 하는 내용으로 Binary Encoding(이진 부호화) 방식 중 하나이다. 물론 스키마 레지스트리에서 에이브로 이외의 이진 부호화 방식도 지원하지만 이 글에서는 에이브로만 설명한다.

에이브로는 머신 간 데이터 전달을 위해 부호화 하는데, 에이브로 안에 스키마를 포함하며 데이터는 Binary 형태로 만들어진다. 먼저 다음과 같은 데이터를 전달하고 싶다고 해보자. Json 형태의 데이터이다.

{
    "userName": "Martin",
    "favoriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}

전달하고자 하는 데이터의 스키마를 에이브로는 다음과 같이 정의한다.

{
    "type": "record",
    "name": "Person",
    "fields": [
        {"name": "userName",        "type": "string"},
        {"name": "favoriteNumber",  "type": ["null", "string"], "default": null},
        {"name": "interests",       "type": {"type": "array", "items": "string"}}
    ]
}

각 데이터 필드에 타입, default 값 등을 지정할 수 있다. 스키마에 따라 데이터를 만들고 아래와 같이 Binary 형태로 부호화 한다. 부호화한 바이트열을 간단히 살펴보면 스키마에 나타난 필드 순으로 바이트열이 연결된 것을 볼 수 있다.

에이브로 및 다른 이진 부호화 타입에 대해서는 Martin Kleppman의 Schema evolution in Avro, Protocol Buffers and Thrift 를 참고하면 좋다.


2. 스키마
스키마는 에이브로 설명때 말한 것처럼 부호화 하기 위한 데이터 형태이다. 스키마에서 추가적으로 살펴볼 것은 쓰기/ 읽기 스키마이다. 쓰기 스키마는 부호화할 데이터에 사용할 스키마로서 카프카 프로듀서가 사용한다. 읽기 스키마는 부호화 된 데이터를 복호화할 때 카프카 컨슈머가 사용한다.
이 때, 쓰기/ 읽기 스키마는 동일한 버전이지 않아도 된다. 이 부분이 중요한데 동일하지 않은 스키마 버전일지라도 호환성을 유지 할 수 있도록 Schema Evolution(스키마 발전)을 지원한다. Schema Evolution을 위해 필요한 시스템이 스키마 레지스트리이다.

 

Schema Evolution에는 BACKWARD, FORWARD, FULL 등이 있다.  

  • BACKWARD : 상위 버전의 스키마를 쓰는 컨슈머가 하위 버전의 스키마를 쓰는 프로듀서와 호환된다.
  • FORWARD : 상위 버전의 스키마로 전달한 프로듀서의 데이터를 하위 버전의 컨슈머가 호환된다.
  • FULL : 둘다 지원

Schema Evolution의 세부 내용은 Schema Evolution and Compatibility를 참고하자.

 

 

3. 스키마 레지스트리
스키마 레지스트리는 카프카로 전달된 레코드의 스키마를 관리하는 시스템이다.
아래 그림이 카프카와 스키마 레지스트리의 관계를 한번에 보여주는 그림이다.

프로듀서는 스키마를 스키마 레지스트리에 등록하고 데이터를 카프카로 전달한다.
컨슈머는 스키마 레지스트리에 있는 스키마를 가져와서 복호화한다.
이 글에서는 간략히만 설명하고 자세한 사항은 컨플루언트 문서(Schema Registry Overview)를 확인하자.

 

 

4. 컨슈머가 GenericRecord 사용 시 스키마 처리
이번 챕터가 이야기하려는 주제였는데, 앞 부분의 배경 설명이 길었다. 
앞서 컨슈머는 특정 버전의 스키마를 사용해서 복호화를 한다고 했다. 그런데 특정 버전의 스키마 없이 GenericRecord를 통해서 프로듀서가 보낸 스키마 그대로 받을 수도 있었다. 그 차이점을 설명하려고 한다.

먼저 컨슈머가 특정 버전의 스키마를 사용한 코드이다.

주의할 설정은 KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG 이다.  해당 값은 "specific.avro.reader" 인데, true 이면 컨슈머가 지정한 버전의 스키마를 사용한다. 그럼 컨슈머가 특정 스키마 없이 프로듀서의 스키마 그대로 가져오는 방법은 무엇일까? "specific.avro.reader"를 false로 설정하고(defulat가 false) GenericRecord를 사용하면 된다.

GenericRecord를 사용할 때는 스키마 가져올 때 어떤 차이가 있는지 확인하려면 KafkaAvroDeserializer.class를 설펴보면 된다. 아래 코드는 KafkaAvroDeserializer.class의 getReaderSchema 함수인데 읽기 스키마를 가져오는 부분이다.

이 함수의 아규먼트 writerSchema에 consumer가 사용할 스키마가 전달된다. GenericRecord로 레코드를 가져올 때는 레코드를 쓸 때 사용한 스키마가 그대로 들어온다. 반면 특정 버전의 스키마를 사용한 컨슈머는 writerSchema에 지정한 버전의 스키마가 들어온다. 그리고 "specific.avro.reader" 설정을 가지고 있는 변수가 useSpecificAvroReader 이다. 아래 조건문을 보면 useSpecificAvroReader가 true 일 때는 스키마를 캐시에 담아둔다. 그래서 다음에 들어오는 레코드는 캐시에 저장한 스키마를 그대로 사용해서 처리한다. 하지만 GenericRecord는 캐시를 사용하지 않는다.


5. 정리
이번글에서는 avro로 들어오는 데이터를 처리할 때 컨슈머의 스키마 관리에 대해서 알아봤다. 서비스 요구사항에 맞춰 컨슈머가 명확한 버전의 스키마를 사용할 경우와 그렇지 않을 경우의 환경을 잘 고려해서 GenericRecord를 활용하면 된다. 기본적으로 avro를 사용하면 string으로레코드를 처리할 때 발생하는 파싱 오류를 피할 수 있으며 Schema Evolution의 장점도 활용할 수 있다. 그리고 데이터 사이즈도 줄일 수 있다. string으로 강제되어야 하는 상황이 아니면 avro를 사용하는 것이 좋은 선택으로 보여진다.

 

참고 문서

 

반응형