[데이터 중심 애플리케이션 설계] 4. 부호화와 발전

succeeding·2024년 11월 23일
post-thumbnail

목표

  • JSON, XML, Protocol Buffers, Thrift, Avro 등 데이터 부호화(encoding) 혹은 직렬화(serialization) 에 대해 알아본다.
  • 각 부호화 형식이 어떻게 하위 호환성과 상위 호환성을 지원하는지 설명한다.
  • REST, RPC, message queue 에서 데이터 부호화 형식이 데이터 저장과 통신에 어떻게 사용되는지 살펴본다.

내용

데이터 부호화 형식

Language-Specific Formats

java.io.Serializable 같은 java 의 내장 직렬화는 성능이 좋지 않고 비대해지곤 한다. 이런 이유로, 언어에 내장된 부호화를 사용하는 것은 보통 좋지 않다.

JSON 과 XML, 이진 변형

결함

  • JSON 은 정수와 부동소수점 수를 구별하지 않으며, 정밀도를 지정하지 않는다. 2^53 보다 큰 정수는 IEEE 754 부동소수점 수에서는 정확하게 표편할 수 없다. 이로 인해, 부동소수점 수를 사용하는 언어에서는 파싱할 때 수가 부정확해질 수있다.
  • JSON, XML은 이진 문자열(byte array)를 지원하지 않는다. 보통 이진 데이터를 Base64 를 사용해 텍스트로 encoding 하여 이런 제한을 피한다. 이 방법은 다시 Base64 로 decoding 해야하고, 데이터 크기가 33% 증가한다는 단점이 있다.(Base64는 3바이트의 이진 데이터를 4바이트의 문자로 encoding 하는 방법이다.)
  • JSON과 XML을 정의하는 스키마 언어가 있지만 익히고 구현하기가 상당히 난해하다.

그럼에도 JSON, XML, CSV 는 다양한 용도에서 사용하기 충분하고, 앞으로도 인기가 많을 것이다.

이진 부호화(binary encoding)

JSON, XML 은 이진 형식에 비해 더 많은 공간을 사용한다. 이에 따라 JSON, XML 용의 다양한 이진 부호화(binary encoding) 개발이 이루어졌다.

그러나 JSON 과 XML 의 텍스트 버전처럼 널리 채택되지 않았다. 이진 부호화가 가독성을 떨어뜨리는 것에 비해 텍스트 부호화에 비해 공간을 절약하는 정도가 그리 크지 않다는 의견이다.

Thrift 와 Protocol Buffers

Apache Thirft(by Facebook) 와 Protocol Buffers(protobuf, by Google) 는 같은 원리를 기반으로 한 이진 부호화 라이브러리이다.

스키마를 정의하면, 해당 스키마로 다양한 프로그래밍 언어로 스키마를 구현한 클래스를 생성한다. 이 클래스로 스키마의 레코드를 이진 부호화/복호화할 수 있다.

부호화된 데이터는 필드의 이름이 아닌 스키마에서 필드에 정의한 숫자인 필드 태그(field tag) 를 사용한다. 이를 통해 부호화된 데이터의 크기를 더욱 줄일 수 있다.

syntax = "proto3";

package demo;

message Simple {
  string foo = 1; // ← 이 숫자 1이 "필드 태그(field number)"
}

Thrift 에는 BinaryProtocol과 CompactProtocol이 있다. CompactProtocol이 더 적은 용량을 사용한다.

Thrift의 BinaryProtocol

  • 각 필드가 (타입 + 필드 태그 + 길이(optional) + 값) 조합의 byte array로 encoding 됨
  • 값이 문자열인 경우 아스키 혹은 UTF-8로 encoding한다.

Thrift의 CompactProtocol

  • (타입 + 필드 태그)을 단일 바이트로 압축
  • 정수는 가변 길이 enciding 방식 을 사용해서 부호화

가변 길이 정수 메커니즘
7비트씩 자르고 최상위 비트엔 지속 비트(continuation bit) 를 사용하여 뒤에 바이트가 더 오는지 알려줌

  • 각 바이트 형식: c | b7 b6 b5 b4 b3 b2 b1
    • b1 ~ b7: 데이터 빝츠
    • c: continuation bit. 1비트면 뒤에 바이트가 더 오고 0이면 마지막 바이트
  • 예시
    • 300 = 1 0010 1100(0b)
    • 아래쪽부터 7비트씩: 0010 1100(0x2C), 나머지 0000 0010(0x02)
    • 첫 바이트에 “더 있음” 표시로 MSB=1 → 0x2C | 0x80 = 0xAC
    • 두 번째 바이트는 마지막이므로 MSB=0 그대로 0x02
    • 전송 바이트: AC 02

Protobuf 는 Thrift 의 CompactProtocol와 매우 비슷하게 동작한다.

필드 태그와 스키마 발전

스키마 발전(schema evolution): 스키마가 시간이 지남에 따라 변하는 것

Thrift 와 Ptotobuf 에서 어떻게 상위호환성/하위호완성을 달성하면서 스키마 발전을 할 수 있을까?

필드명 수정
부호화된 데이터엔 필드명이 없고 필드 태그만 있기 때문에, 필드 태그는 수정하지 않되, 해당 태그의 필드명을 스키마에서 수정할 수 있다.

필드 추가
필드에 새로운 태그 번호를 부여하여 스키마에 새로운 필드를 추가할 수 있다.

상위 호환성 달성 필드 추가
새로 추가된 태그 번호는 이전 스키마를 가지고 있는 예전 코드에선 단순히 부호화에서 누락시키기 때문에, 상위호환성을 달성할 수 있다.

하위 호환성 달성 필드 추가
새로운 필드에 required 를 사용해선 안된다. required 를 사용하면, 새로운 스키마를 가지고 있는 새 코드가 옛 스키마의 데이터 복호화에 실패하고 만다. required 가 아닌 optional 이나 기본값으로 새로운 필드를 추가해야한다.

필드 삭제
필드 삭제는 필드를 추가할 때, 하위 호환성/상위 호환성 문제를 해결하는 방식과 반대로 하면 된다. 즉, optional 필드만 삭제할 수 있고 같은 태그 번호는 절대 다시 사용할 수 없다.

데이터 타입과 스키마발전

필드의 데이터 타입을 바꾸는 것은 위험하다.
32 비트 정수를 64 비트 정수로 바꾸는 것은 하위 호환성을 가능하나 상위 호환성은 깨지게 된다.

Protobuf 에서 optional 표시자를 repeated 로 변경하는 것은 상위 호환성과 불안전한 하위 호환성을 만족시킬 수 있다. repeated 는 같은 필드가 여러번 부호화 데이터에 나타날 수 있다는 것을 의미하며 프로그래밍 언어에서는 배열로 표시된다. optional 필드가 레코드에 존재하지 않으면 repeated 에서는 원소가 없는 배열로 읽게 되는 것이다. optional 을 사용한 예전 코드에서는 원소 중 마지막 원소만 보게 된다.

Avro

Thrift 와 Protocol Buffers와는 다른 또 하나의 binary encoding 형식

  • 샘플의 부호화 크기는 32바이트로 가장 작다.

Avro history

  • thrift가 하둡의 유즈 케이스에는 부적합하다고 판단하여 2009 년 하둡의 하위 프로젝트로 시작
    • thrift의 어떤 점들이 하둡에 부적절했을까?

Avro 스키마 작성 방법

  • JSON: 이 방식이 기계가 더 쉽게 읽을 수 있다고 함. 레지스트리엔 JSON 스키마만 등록함. 툴체인들도 JSON 형태를 더 잘 지원함.
  • Avro IDL: 사람 친화적 작성용.

읽기, 쓰기 스키마의 구분

  • 쓰기 스키마(wirter's schema)
    • 어떤 데이터를 avro로 encoding하길 원할 때 사용하는 스키마
  • 읽기 스키마(reader's schema)
    • 어떤 avro 데이터를 decoding하길 원할 때 사용하는 스키마

사용자가 작성한 스키마는 읽기 스키마로 사용될 수도 있고, 쓰기 스키마로 사용될 수도 있음

encoding된 바이트배열

  • 필드나 데이터 타입을 식별하기 위한 정보가 없음
  • 단순히 값으로 구성됨
  • 정수의 경우 가변 길이 encoding을 사용한다. thrift의 컴팩트 프로토콜과 같다.

이런 avro 데이터를 decoding하려면

  • 스키마에 나타난 순서대로 필드를 살펴보아야 한다. 필드 순서대로 encoding하는 것은 쓰기 스키마를 이용한 encoding이 보장.
  • 데이터를 읽는 코드가 데이터를 encoding한 코드와 정확히 같은 스키마를 사용해야 안전하게 encoding 가능함. 스키마 호환성을 지키면서 진화한 스키마라면 대체해서 사용해도 괜찮음.
    • 스키마 진화가 없다면 하나의 쓰기 스키마로 decoding까지 가능(쓰기 스키마가 읽기 스키마 역할까지 함)
    • 스키마 진화가 있다면, 읽기 스키마, 쓰기 스키마 모두 필요함

아브로의 핵심 아이디어

  • 쓰기 스키마와 읽기 스키마가 동일하지 않아도 되며, 호환 가능하기만 하면 됨
  • 복호화(읽기)할 때, 쓰기 스키마에서 읽기 스키마로 데이터를 변환해 그 차이를 해소함

스키마 해석(resolution): 읽기, 쓰기 스키마 간의 차이를 해소하는 과정

  • 이름으로 필드를 일치시키기 때문에, 쓰기 스키마와 읽기 스키마의 필드 순서가 달라도 문제 없음
  • 쓰기 스키마에만 존재하는 필드는 읽기에서 무시 됨
  • 읽기 스키마에만 존재하고 데이터에 해당 필드가 없는 경우 기본값으로 채움

스키마 발전 규칙

Confluent Cloud doc - schema evolution
BACKWARD: 하위 호환성(V1 - read → V2)

  • 이전 버전의 쓰기 스키마를 새로운 버전의 읽기 스키마로 읽을 수 있음
    FORWARD: 상위 호환성(V1 ← read - V2)
  • 새로운 버전의 쓰기 스키마를 이전 버전의 읽기 스키마로 읽을 수 있음

null 타입을 사용하기 위해선 유니온 타입(union type) 을 사용해야 한다.

  • “type” [”null”, “string”]

필드의 데이터 타입 변경이 가능하다. 호환성은 깨질 수 있다.

필드 이름 변경은 가능하지만, 상위 호환성은 깨진다.

  • 변경 방법
    • 읽기 스키마는 필드 이름의 별칭(alias)으로 이전 쓰기 스키마 필드 이름을 적으면 된다.

필드 삭제는 각 호환성에 따라 조건부로 가능하다.

  • BACKWARD: 삭제 가능. 새 스키마에서 없는 필드는 무시하기 때문.
  • FORWARD: default가 있는 경우만 가능. 새 스키마로 쓰여져서 해당 필드가 없으면 이전 스키마에선 default 값으로 채워넣음.

코드 생성과 동적 타입 언어

thrift와 protocol buffer는 코드 생성에 의존한다. 즉, 스키마를 정의한 후 선택한 프로그래밍 언어로 스키마를 구현한 코드를 생성한다.

  • 이 방식은 정적 언어에선 효율적인 인메모리 구조를 사용하고 IDE에서 타입 확인과 자동 완성이 가능하기에 유용하다.
  • 그러나, JS, Python같은 동적 프로그래밍 언어에서는 컴파일 시점에 타임 검사기가 없기 때문에 코드를 생성하는 것이 중요하지 않다.

avro는 정적 타입 프로그래밍 언어를 위해 코드 생성을 선택적으로 제공한다. 하지만 코드 생성 없이도 사용할 수 있다. avro파일만 있으면 즉시 데이터 파일을 열어서 분석을 시작할 수 있다.

스키마의 장점

Modes Of Dataflow

Dataflow Through Databases

Dataflow Through Services: REST and RPC

Message-Passing Dataflow

0개의 댓글