Avro, Compatibility
by Gunju Ko
Data Serialization and Evolution
네트워크를 통해 데이터를 전송하거나 파일에 저장할 때 데이터를 바이트로 인코딩해야 한다. 데이터 직렬화 영역은 오랜 역사가 있지만 지난 몇년 동안 상당히 발전했다. 초기에는 Java 직렬화와 같은 프로그래밍 언어 전용 직렬화를 사용했기 때문에 다른 언어에서 해당 데이터를 읽기 어려웠다. 그런 다음 JSON과 같이 언어에 구애받지 않는 형식으로 이동했다.
그러나 JSON과 같은 형식에는 엄격하게 정의된 형식이 없기 때문에 아래와 같은 두가지 중요한 단점이 있다.
- 데이터 컨슈머가 데이터 프로듀서를 이해하지 못할 수 있다
구조가 없으면 필드를 임의로 추가하거나 제거할 수 있고 데이터가 손상될 수도 있다. 이런 단점은 데이터를 Consuming하는 것을 더욱 어렵게 만든다. 이러한 단점은 데이터를 여러 어플리케이션이나 팀들이 같이 사용할때 더 심각해진다. 데이터를 생성하는 어플리케이션에서 데이터 형식이 변경된 경우에 데이터를 소비하는 어플리케이션이 변경된 데이터 포맷을 해석할 수 있을지에 대한 것을 보장할 수 없다. 이러한 문제가 발생하는 이유는 프로듀서와 컨슈머 사이의 일종의 계약이 없기 때문이다.
- Overhead and verbosity : 모든 메시지에서 필드의 타입과 이름이 동일함에도 불구하고 필드 이름과 타입 정보가 직렬화된 포맷에 명시적으로 나타나기 때문에 verbose하다.
스키마에 의해 공식적으로 정의된 데이터 구조를 필요로하는 직렬화 라이브러리가 등장했다. 대표적으로 Avro, Thrift 그리고 Protocol Buffers가 있다. 스키마가 있는 이점은 데이터의 구조, 타입 및 의미를 명확하게 지정할 수 있다는 것이다. 또한 스키마를 사용하면 데이터를 보다 효율적으로 인코딩할 수 있다.
Avro
Avro 스키마는 JSON 형식으로 데이터 구조를 정의한다.
아래의 예는 간단한 Avro 스키마이다. 유저 레코드는 name과 favorite_number라는 2개의 필드로 구성되어 있으며 각각의 타입은 String, int 이다.
{"namespace": "example.avro",
"type": "record",
"name": "user",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": "int"}
]
}
Avro 스키마를 사용하여 Java 객체를 바이트로 직렬화(serialize)하고 바이트를 Java 객체로 역직렬화(deserialize) 할 수 있다. Avro에 대한 흥미로운 점 중 하나는 데이터 직렬화 중에 스키마가 필요할 뿐만 아니라 역직렬화 중에도 필요하다는 것이다. 스키마는 디코딩시에 제공되므로 필드 이름과 같은 메타 데이터를 데이터에 명시적으로 인코딩 할 필요가 없다. 따라서 Avro 데이터의 binary 인코딩이 매우 compact 해진다.
Schema Evolution
데이터 관리의 중요한 측면은 스키마 진화이다. 초기 스키마가 정의 된 이후에 시간이 지남에 따라 스키마를 진화시켜야 할 수도 있다. 이러한 상황이 발생하더라도 컨슈머는 이전 스키마로 인코딩된 데이터와 최신 스키마로 인코딩 된 데이터 모두 원활하게 처리 할 수 있어야 한다. 이는 문제가 발생하기 전까지는 간과되기 쉬운 영역이다. 신중하게 데이터 관리와 스키마 진화를 생각하지 않는다면 나중에 훨씬 높은 비용을 지불하게 된다.
스키마 진화의 세 가지 일반적인 패턴이 있다.
- backward compatibility
- forward compatibility
- full compatibility
Backward Compatibility
Backward Compatibility는 이전 스키마로 인코딩 된 데이터를 새로운 스키마로 읽을 수 있음을 의미한다.
카프카의 모든 데이터가 HDFS에 로드되는 경우를 생각해봐라. 그리고 데이터에 대해 (예 : Apache Hive를 사용하여) SQL 쿼리를 실행하고 싶다. 여기서 중요한 것은 시간이 지남에 따라 데이터가 변경되더라도 동일한 SQl 쿼리가 계속해서 실행되어야 한다는 것이다. 이런 경우에는 Backward Compatibility 방식으로 스키마를 진화시킬 수 있다. Avro에는 backward compatible 방식으로 스카마를 진화시키기 위해서 새 스키마에 허용되는 변경 사항에 대한 규칙 집합이 있다. 만약 모든 스키마가 backword compatible 방식으로 진화된다면, 최신 스키마를 사용하여 모든 데이터를 균일하게 쿼리 할 수 있다.
아래의 예제 처럼 “favorite_color” 필드를 추가해서 스키마가 진화되는 경우를 보자. 새로운 필드는 디폴트 값을 가지고 있다. 기본값은 “green” 이다. 추가된 필드가 디폴트 값을 가지고 있는 경우에는 이전 스키마로 인코딩 된 데이터를 새로운 스키마로 읽을 수 있다. 이전 스키마로 인코딩 된 데이터를 새로운 스키마로 역직렬화 할 때 새롭게 추가된 필드는 디폴트값으로 세팅해서 역직렬화한다.
만약에 새로운 스키마에 추가된 필드에 디폴트값이 없다면 Backward Compatibility 하지 않다. 왜냐하면 이전 데이터에는 추가된 필드에 대한 값이 없기 때문에, 이전 데이터를 새로운 스키마로 역직렬화할 때 추가된 필드에 어떤값을 할당해야 할 지 모른다.
Note 만약에 이전 스키마로 인코딩된 데이터를 새로운 스키마로 어떻게 디코드 하는지에 대한 세부 구현이 궁금하다면 ResolvingDecoder 코드를 참고하길 바란다.
version 1
{"namespace": "example.avro",
"type": "record",
"name": "user",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": "int"}
]
}
version 2
{"namespace": "example.avro",
"type": "record",
"name": "user",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": "int"},
{"name": "favorite_color", "type": "string", "default": "green"}
]
}
Forward Compatibility
Forward Compatibility는 새로운 스키마로 인코딩 된 데이터를 이전 스키마로 읽을 수 있음을 의미한다.
컨슈머가 특정 스키마버전에 의존하는 어플리케이션 로직을 가지고 있다고 가정해보자. 스키마가 진화하더라도 어플리케이션 로직이 즉시 업데이트 되지 않을 수 있다. 따라서 새로운 스키마로 인코딩된 데이터를 이전 버전의 스키마로 투영할 수 있어야 한다. 이를 위해서는 Forward Compatibility 방식으로 스키마를 진화시켜야 한다.
예를 들어 아래와 같이 스키마를 진화시켰다고 가정해보자. version1에서 version2로 스키마가 진화하면서 “favorite_color” 필드가 추가되었다. 이 경우는 Forward Compatibility 하다. 왜냐하면 새 스키마(version2)로 작성된 데이터를 이전 스키마(version1)로 투영 할 때 새 필드(favorite_color)는 단순히 삭제된다.
version1에서 favorite_number와 같이 기본값이 없는 필드를 삭제한다면 Forward Compatibility 하지 않는다. 왜냐하면 새 스키마로 작성된 데이터를 이전 스키마로 투영할 때 favorite_number 값을 어떤 값으로 채울지 모르기 때문이다. 이 문제는 기본값을 명시함으로써 해결할 수 있다.
version 1
{"namespace": "example.avro",
"type": "record",
"name": "user",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": "int"}
]
}
version 2
{"namespace": "example.avro",
"type": "record",
"name": "user",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": "int"},
{"name": "favorite_color", "type": "string", "default": "green"}
]
}
Full Compatibility
Full compatibility는 Forward Compatibility + Backward Compatibility 이다.
Full compatibility 방식으로 스키마를 진화시킨다면 두 가지 모두가 가능하다.
- Forward Compatibility : 새로운 스키마로 인코딩 된 데이터를 이전 스키마로 읽을 수 있다.
- Backward Compatibility : 이전 스키마로 인코딩 된 데이터를 새로운 스키마로 읽을 수 있다.
Schema Registry
보시다시피, Avro를 사용할 때 가장 중요한 것 중 하나는 스키마를 관리하고 스키마 진화 Compatibility를 결정하는 것이다. Confluent Schema Registry는 그 목적으로 만들어졌다. API Reference를 보면 Avro Schema를 어떻게 저장할 수 있는지, 스키마 진화 중 특정 Compatibility를 어떻게 적용하는지에 대한 자세한 설명을 볼 수 있다.