Study

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

mokpolar 2022. 4. 2. 22:25
반응형

이 글은 도서 "데이터 중심 애플리케이션 설계" 를 가지고 스터디모임을 하면서 4장 - 부호화와 발전을 정리한 내용을 옮긴 글입니다.

 

데이터 중심 애플리케이션 설계

데이터를 처리하고 저장하는 다양한 기술의 장단점을 검토한다. 소프트웨어는 계속 변하지만 근본 원리는 동일하다. 이 책에서 소프트웨어 엔지니어와 아키텍트는 실전에 이 개념을 어떻게 적

www.aladin.co.kr

 

부호화와 발전 Encoding & Evolution

 

이 장의 핵심 문구 : 

💡 여러가지 이유로 Application은 변하게 된다. 그리고 Application 기능을 변경하기 위해서는 저장하는 데이터도 변경해야 한다.

 

서버, 클라이언트 측 어플리케이션이 계속 유동적으로 변하기 때문에 예전 버전 코드와 새로운 버전의 코드, 이전의 데이터 타입과 새로운 데이터 타입이 모든 시스템에서 공존할 수 있기 때문에 상/하위 호환성을 고려해야 한다.

 

상/하위 호환성에 대해 직관적으로 이해할 수 있는 그림

출처 : https://stevenheidel.medium.com/backward-vs-forward-compatibility-9c03c3db15c9

  • 하위 호환성 Backward Compatibility
    • Backward compatibility means that readers with a newer schema can correctly parse data from writers with an older schema.
    • 새로운 코드는 예전의 코드가 기록한 데이터를 읽을 수 있어야 한다. Newer code can read data that was written by older code.
    • 새로운 코드는 예전 버전의 코드가 기록한 데이터 형식을 알기에 명시적으로 해당 형식을 다룰 수 있다.
  • 상위 호환성 Forward Compatibility
    • Forwards compatibility means that readers with an older schema can correctly parse data from writers with a newer schema.
    • 예전 코드는 새로운 코드가 기록한 데이터를 읽을 수 있어야 한다. Older code can read data that was written by newer code.
    • 더 어렵다. 예전 버전의 코드가 새 버전의 코드에 의해 추가된 것을 무시할 수 있어야 하므로

데이터 부호화 형식 Formats for Encoding Data

  • 프로그램은 (최소한) 두 가지 형태로 표현된 데이터를 사용해 동작한다.
    • 메모리에 객체 Object, 구조체 Struct, 목록 List, 배열 Array, 해시 테이블 Hash table, 트리 Tree로 유지
    • 이런 데이터 구조는 CPU에서 효율적으로 접근하고 조작할 수 있게 최적화
  • 데이터를 파일에 쓰거나 네트워크를 통해 전송하려면 JSON과 같은 바이트열의 형태로 부호화 Encoding 해야 한다.
    • 포인터는 다른 프로세스가 이해할 수 없다.
    • 그래서 이 일련의 바이트열은 보통 메모리에서 사용하는 데이터 구조와는 상당히 다르다.

출처 : https://medium.com/tech-learnings/serialization-filtering-deserialization-vulnerability-protection-in-java-349c37f6f416

  • 위 그림과 같이 인메모리 표현에서 바이트열로의 전환을
    • 부호화 Encoding
    • 직렬화 Serialize
    • 마샬링 Marshalling
  • 바이트 열에서 인메모리 표현으로 전환을
    • 복호화 Decoding
    • 역직렬화 Deserialize
    • 언마샬링 Unmarshalling

언어별 형식

프로그래밍 언어들은 인메모리 객체를 바이트열로 부호화하는 기능을 내장함. 예들은 아래와 같음.

  • Java
    • java.io.Serializable
  • Ruby
    • Marshal
  • Python
    • Pickle
  • Java의 써드파티 라이브러리
    • Kryo
  • 다른 언어에서 읽기가 어렵기 때문에 프로그래밍 언어가 고정되며 다른 시스템과 통합시 방해가 된다.
  • 복호화 Deserialize 과정이 임의의 클래스를 인스턴스화 할 수 있어야 하는데, 보안상 문제가 될수도.
  • 빠른 부호화를 위해 상/하위 호환성의 불편한 문제를 뒷전으로 하기도.
  • 부호화, 복호화에 소요되는 CPU 시간이나 부호화된 구조체의 크기 같은 효율성도 뒷전으로 하기도..
    • 자바 내장 직렬화는 그닥 성능이 좋지 않은데 아래와 같다. 
    • 직렬화 후 바이트 크기

  • 직렬화 소요시간

JSON과 XML, 이진 변형 Binary Variants

JSON이 XML 대비 단순하고, 웹 브라우저에 내장된 지원 때문에 인기가 더 많다. 그리고 CSV도 있다.

  • 위 문장과 관련하여 아래 문서는 JSON VS XML VS CSV
    • 각각의 장단점을 비교한 문서 아래 링크 참조
    • 해당 문서의 결론
    • 일반적으로 JSON은 현재까지 최고의 데이터 교환 형식입니다. 가볍고 컴팩트하며 다목적입니다. CSV는 엄청난 양의 데이터를 전송하고 대역폭이 문제인 경우에만 사용해야 합니다. 오늘날 XML은 문서 마크업에 더 적합하기 때문에 데이터 교환 형식으로 사용되어서는 안 됩니다.
    • CSV vs XML vs JSON - Which is the Best Response Data Format?
 

CSV vs XML vs JSON – Which is the Best Response Data Format? | Digital Hospital

Whether you are building a thin client (web application) or thick client (client-server application) at some point you are probably making requests to a web server and need a good data format for responses. As of today, there are three major data formats b

digitalhospital.com.sg

 

  • XML, CSV에서는 수와 숫자 digit로 구성된 문자열을 구분할 수 없다.
  • 2^53을 넘어가면 부동 소수점 문제가 발생한다.
    • twitter는 이 문제를 해결하기 위해 64bit 숫자를 사용한다
  • JSON과 XML은 사람이 읽을 수 있는 유니코드 문자열을 잘 지원한다. 그러나 이진 문자열을 지원하지 않는다.
    • 이진 문자열은 유용하다. 그래서 이진 데이터를 Base64를 사용해 텍스트로 부호화 Encoding해 이 제한을 피한다.
    • 그리고 값이 Base64로 부호화 되었다는 사실을 스키마를 사용해 표시
    • 그리고 데이터 크기가 33% 증가
  • CSV는 스키마가 없어서 각 로우와 컬럼의 의미를 정의하는 작업은 앱이 해야 한다.

이진 부호화 Binary encoding

작은 데이터셋이라면 무시할 수 있지만 “테라바이트 정도가 되면 데이터 타입의 선택이 큰 영향을 미친다!”

  • 데이터 공간은 이진 형식 < JSON, XML
  • 이진 부호화의 예제로 쓸 JSON
    • 스키마를 지정하지 않기 때문에 데이터 안에 모든 객체의 필드 이름을 포함해야 한다.
    • 이 경우 userName, favoriteNumber, interests 라는 string을 포함해야 한다.
{
        "userName": "Martin",
        "favoriteNumber": 1337,
        "interests": ["daydreaming", "hacking"]
}

메세지 팩 Message Pack

  • JSON 용 이진 부호화 형식
  • 위의 JSON을 Message Pack으로 Encod해서 얻은 Byte Sequence이다.

  • 메세지팩에 대한 상세한 내용은 아래 링크를 참조

msgpack/spec.md at master · msgpack/msgpack

 

GitHub - msgpack/msgpack: MessagePack is an extremely efficient object serialization library. It's like JSON, but very fast and

MessagePack is an extremely efficient object serialization library. It's like JSON, but very fast and small. - GitHub - msgpack/msgpack: MessagePack is an extremely efficient object serializati...

github.com

스리프트와 프로토콜 버퍼 Thrift and Protocol Buffers

아파치 스리프트 Apache Thrift, 프로토콜 버퍼 Protocol Buffers (protobuf)

둘다 부호화할 데이터를 위한 스키마가 필요하다.

다시, 아래를 부호화 Encoding 하기 위해서는

{
        "userName": "Martin",
        "favoriteNumber": 1337,
        "interests": ["daydreaming", "hacking"]
}
  • 스리프트
    • 스리프트 인터페이스 정의 언어
    • IDL interface definition language
    • 로 스키마를 기술해야 한다.
struct Person {
  1: required string       userName,
  2: optional i64          favoriteNumber,
  3: optional list<string> interests
}
  • 프로토콜 버퍼
    • 스리프트와 매우 비슷함
message Person {
    required string user_name       = 1;
    optional int64  favorite_number = 2;
    repeated string interests       = 3;
}

위의 두 정의를 이용해 코드를 생성하는 도구를 가지고 다양한 프로그래밍 언어로 스키마를 구현한 클래스를 생성

어플리케이션은 생성된 코드를 호출해 스키마의 레코드를 부호화, 복호화할 수 있다.

  • 스리프트 바이너리 프로토콜 BinaryProtocol
    • 아래와 같으며 59바이트

  • 스리프트 컴팩트프로토콜 CompactProtocol
    • 아래와 같으며 34바이트
    • 필드 타입과 태그 숫자를 단일 바이트로 줄임
    • 그리고 가변 길이 정수 variable-length integer를 사용해서 부호화
    • 숫자 1337도 2바이트로 부호화 (큰 수는 더 많은 바이트로)

  • 프로토콜 버퍼
    • 컴팩트 프로토콜과 비슷하고 33바이트

  • 아래를 다시 보면 required와 optional이 명시되어 있다.
  • 부호화하는 방법에는 차이가 없으나 required를 사용하면 필드가 설정되지 않은 경우를 실행시에 확인할 수 있다고 함
struct Person {
  1: required string       userName,
  2: optional i64          favoriteNumber,
  3: optional list<string> interests
}
  • 아래는 위 내용과 관련하여 Protocol Buffers의 Python tutorial

Protocol Buffer Basics: Python | Protocol Buffers | Google Developers

 

Protocol Buffer Basics: Python  |  Protocol Buffers  |  Google Developers

Protocol Buffer Basics: Python This tutorial provides a basic Python programmer's introduction to working with protocol buffers. By walking through creating a simple example application, it shows you how to Define message formats in a .proto file. Use the

developers.google.com

  • 아래와 같은 스키마를 작성하고
syntax = "proto2";

package tutorial;

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}
  • 다운로드 받은 프로토콜 버퍼 컴파일러로
    • 아래와 같이 실행하면
    • addressbook_pb2.py가 생성된다.
    • 이는 메시지를 읽고 쓰는데 필요한 Python 클래스임
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto

필드 태그와 스키마 발전 Datatypes and schema evolution

스키마는 필연적으로 시간이 지남에 따라 변하고 이를 Schema Evolution 스키마 발전 이라고 부른다.

  • 부호화된 레코드는 부호화된 필드의 연결이고
    • 각 필드는 태그 숫자로 식별하며 데이터 타입을 주석으로 단다.
    • 필드 값을 설정하지 않으면 부호화 레코드에서 생략한다.
  • 필드 태그가 없으면 기존의 모든 부호화된 데이터를 인식 불가능할 수 있기 때문에 변경 불가능하다.
  • 태그 번호를 추가하는 것으로 스키마에 새로운 필드를 추가할 수 있는데
  • 예전 코드 v1은 (새로운 태그 번호 추가에 대해 알지 못함)
  • 새로운 코드 V2로 기록한 데이터를 읽으려고 할 때 (v1 코드가 모르는 태그 번호를 가진 필드가 있음)
  • 해당 필드를 무시한다. 즉, 상위 호환성 Forward Compatibility가 유지된다.

  • 각 필드 태그 번호가 있는 동안에는 같은 의미를 유지하기에 새로운 코드도 항상 예전 데이터를 읽을 수 있다.
  • 즉, 하위 호환성 Backward Compatibility가 유지된다.

  • 하지만 새로운 필드를 추가했을 때 required로 하면 예전 코드로 기록한 데이터에는 이 필드가 없다. 그래서 새로운 코드가 예전 코드로 기록한 데이터를 읽는 작업은 실패한다.
  • 그러므로 호환성을 위해서는 스키마의 초기 배포후에 추가되는 모든 필드는 optional로 하거나 기본값을 가져야 한다.

데이터타입과 스키마 발전 Datatypes and schema evolution

  • 필드의 데이터 타입을 변경하는건?
    • 가능은 하지만 정확하지 않거나 내용이 잘릴 위험이 있다.
  • 32비트 정수를 64비트로 바꾸는 경우 (새 코드는 64비트로 읽으려고 하는가?)
    • Parser가 누락된 비트를 0으로 채울 수 있기 때문에 새로운 코드는 예전 코드가 기록한 데이터를 쉽게 읽을 수 있다.
      • 예전 코드가 기록한 데이터에 누락된 부분을 0으로 채우기 때문?
    • 새로운 코드가 기록한 데이터를 예전 코드가 읽는 경우
      • 값을 유지하기 위해 32비트 변수를 계속 사용한다
      • 복호화된 64비트 값은 32비트에 맞지 않기 때문에 잘린다.
      • 가능은 한데 잘린다
      • 에러는 안나는데 의미는 이미 깨져있다?
  • 프로토콜 버퍼에는 “목록" 이나 “배열 데이터 타입" 이 없지만
    • 필드에 repeated가 있다.
    • repeated 필드의 부호화는 레코드에 “단순히 동일한 필드 태그가 여러번 나타난다.”
    • 이런 느낌인지?
    message Person {
        repeated string user_name       = 1;
        repeated int64  favorite_number = 1;
        repeated string interests       = 1;
    }
    • optional 필드를 repeated 필드고 변경해도 문제가 없다.
    • 0이나 1개의 엘리먼트가 있는 목록으로 보게 되고
    • 새로운 데이터를 읽는 예전 코드는 “목록의 마지막 엘리먼트만 보게 된다.”

아브로 Avro

  • 프로토콜 버퍼와 스리프트와 대적할 만한다고 함.
  • 부호화할 데이터 구조를 지정하기 위한 스키마를 사용하는 데 두 가지가 있다.
  • 사람이 편집할 수 있는 Avro IDL
record Person {
    string               userName;
    union { null, long } favoriteNumber = null;
    array<string>        interests;
}
  • 기계가 더 쉽게 읽을 수 있는 JSON 기반언어
{
    "type": "record",
    "name": "Person",
    "fields": [
        {"name": "userName",       "type": "string"},
        {"name": "favoriteNumber", "type": ["null", "long"], "default": null},
        {"name": "interests",      "type": {"type": "array", "items": "string"}}
    ]
}
  • 다시 소환
{
        "userName": "Martin",
        "favoriteNumber": 1337,
        "interests": ["daydreaming", "hacking"]
}
  • Avro로 Encoding한 이진레코드는 아래와 같다.

  • 스키마에 태그번호가 없다.
  • 32바이트로 이진 부호화시 길이가 가장 짧다.
  • 필드나 데이터 타입을 식별하기 위한 정보가 없다.
  • 단순 연결된 값이다.
  • 문자열은 길이 다음에 UTF-8 바이트가 이어지지만 문자열임을 알려주는 정보가 없다.
  • 정수는 가변 길이 부호화를 사용해서 부호화된다. (스리프트 컴팩트프로토콜과 같이)
  • 위와 같은 특징 때문에 Avro로 이진 데이터를 파싱하려면
  • 스키마에 나타난 “순서대로" 필드를 살펴보고 스키마를 이용해 각 필드의 데이터 타입을 미리 파악해야 한다.
  • 그러니까 데이터를 읽는 코드가 데이터를 기록한 코드와 “정확히 같은 스키마" 를 사용할 때만 올바르게 복호화 Decoding이 가능하다.

쓰기 스키마와 읽기 스키마 The writer’s schema and the reader’s schema

쓰기 스키마의 경우

  • 애플리케이션이
    • 파일이나 데이터베이스에 쓰기 위해
    • 네트워크를 통해 전송하기 위해
    • 부호화하는 경우에는 “알고 있는" 스키마 버전을 사용해 데이터를 부호화해야 한다.
  • 애플리케이션에는 이 스키마를 포함할 수 있고 이를 쓰기 스키마라고 한다.

읽기 스키마의 경우

  • 애플리케이션이
    • 파일이나 데이터베이스에서
    • 네트워크로부터 수신하기 위해
    • 복호화하는 경우에는 “특정 스키마"로 복호화하길 기대한다.
  • 애플리케이션 코드는 이 스키마에 의존하고 이를 “읽기 스키마"라고 한다.
  • 복호화 코드는 애플리케이션을 빌드하는 동안 “스키마로부터 생성된다.”
  • Avro 라이브러리는 쓰기 스키마와 읽기 스키마를 함께 살펴본 다음
  • 쓰기 스키마 → 읽기 스키마 로 데이터를 변환해 그 차이를 해소한다.

  • 스키마 해석 schema resolution 에서는 “이름"으로 필드를 일치 시킨다.
  • 만약 데이터를 읽는 코드가 읽기 스키마 에는 없고 쓰기 스키마에 존재하는 필드를 만나면 이 필드는 무시한다.
  • 데이터를 읽는 코드가 기대하는 어떤 필드가 쓰기 스키마에는 포함돼 있지 않은 경우에는 읽기 스키마에 선언된 기본값으로 채운다.

예시

출처 : https://ambitious.systems/avro-writers-vs-readers-schema

  • 쓰기 스키마
import json
import avro.schema
writer_schema = avro.schema.parse(json.dumps({
    "namespace": "ambitious.app",
    "type": "record",
    "name": "User",
    "fields": [
        {"name": "first_name", "type": "string"},
        {"name": "last_name",  "type": ["string", "null"]},
        {"name": "email", "type": "string"},
        {"name": "age", "type": ["int", "null"]},
    ]
}))
  • 데이터 추가
from avro.datafile import DataFileWriter
from avro.io import DatumWriter
writer = DataFileWriter(open("users-multiple-schemas.avro", "wb"), DatumWriter(), writer_schema)
writer.append({"first_name": "John", "last_name": "Doe", "email": "john.doe@example.com"})
writer.append({"first_name": "Jane", "age": 23, "email": "jane.doe@example.com"})
writer.close()
  • 읽기 스키마
reader_schema = avro.schema.parse(json.dumps({
    "namespace": "ambitious.app",
    "type": "record",
    "name": "User",
    "fields": [
        {"name": "email", "type": "string"},
        {"name": "first_name", "type": "string"},
        {"name": "age", "type": ["long", "null"]},
        {"name": "timezone", "type": "string", "default": "US/Eastern"}
    ]
}))
  • 여기서 읽기 쓰기마가 다른점은
    • 필드 순서가 변경됨
    • age field가 int 에서 long으로 변경
    • last_name이 없어짐
    • timezone 기본값이 US/Eastern 인 새 필드 가 생김
from avro.datafile import DataFileReader
from avro.io import DatumReader
reader = DataFileReader(
    open("users-multiple-schemas.avro", "rb"),
    DatumReader(readers_schema=reader_schema))
for user in reader:
    print(user)
reader.close()
  • 결과
{'first_name': 'John', 'email': 'john.doe@example.com', 'age': None, 'timezone': 'US/Eastern'}
{'first_name': 'Jane', 'email': 'jane.doe@example.com', 'age': 23, 'timezone': 'US/Eastern'}

스키마 발전 규칙 Schema evolution rules

  • Avro에서
    • 상위호환성은
      • 새로운 버전의 쓰기 스키마와 예전 버전의 읽기 스키마를 가질 수 있음
    • 하위호환성은
      • 새로운 버전의 읽기 스키마와 예전 버전의 쓰기 스키마를 가질 수 있음
  • 다시 소환

  • 호환성을 유지하기 위해서는 “기본값이 있는 필드" 만 추가하거나 삭제할 수 있다.
    • favoriteNumber 필드는 기본값이 null 이었음
  • 기본값이 있는 필드를 추가해서 새로운 스키마에는 추가된 필드가 있고 예전 스키마에는 없으면?
  • 새로운 스키마를 사용하는 읽기가 예전 스키마로 기록된 레코드를 읽으면 누락된 필드는 기본값으로 채워진다.
  • 기본값이 없는 필드를 추가하면 새로운 읽기는 예전 쓰기가 기록한 데이터를 읽을 수 없기 때문에 하위 호환성이 깨진다.
  • 기본값이 없는 필드를 삭제하면 예전 읽기는 새로운 쓰기가 기록한 데이터를 읽을 수 없기 때문에 상위 호환성이 깨진다.
  • 필드에 Null을 허용하려면 유니온 타입 union type을 사용해야 한다.
  • Avro는 optional, required 표시자를 가지지 않고 유니온 타입과 기본값이 있다.
  • 타입 변환이 가능하므로 필드의 데이터 타입 변경도 가능하다.

그러면 쓰기 스키마란 무엇인가? But what is the writer’s schema?

모든 레코드에 스키마를 포함시키면 배보다 배꼽이 더 커진다. 이진 부호화로 절약한 공간이 소용이 없어진다.

상황에 따라 다른데 아래와 같은 경우는 괜찮다.

  • 많은 레코드가 있는 대용량 파일
    • 동일 스키마로 인코딩된 커다란 파일인 경우 (수백만개의 레코드) → 하나만 포함해도 되니까 괜찮음
  • 개별적으로 기록된 레코드를 가진 데이터 베이스
    • 데이터베이스를 따로 두고 스키마 버전 목록을 유지
  • 네트워크 연결을 통해 레코드 보내기
    • 네트워크 연결중에는 동일 스키마를 쓴다고 합의

동적 생성 스키마

관계형 스키마 → Avro 스키마 생성 → 그 스키마를 이용해 데이터베이스 내용을 인코딩 → Avro 객체 컨테이너 파일로 덤프

각 데이터베이스 테이블에 맞게 레코드 스키마를 생성하고 각 칼럼은 해당 레코드의 필드가 됨

데이터베이스의 칼럼 이름은 아브로의 필드 이름에 매핑됨

새로운 데이터 파일을 읽는 사람은 레코드 필드가 변경된 사실을 알게 되지만 필드는 “이름”으로 식별되기 때문에 “갱신된 쓰기 스키마”는 여전히 “이전 읽기 스키마”와 매칭 가능하다.

코드 생성과 동적 타입 언어 ??

  • 스리프트와 프로토콜 버퍼는 코드 생성에 의존한다.
    • 스키마를 정의한 후 선택한 프로그래밍 언어로 스키마를 구현한 코드를 생성할 수 있다.
    • 자바, C++, C# 같은 정적 타입언어에서 유용하다.
      • 복호화 Decoding 된 언어를 위해 효율적인 인메모리 구조를 사용하고 데이터 구조에 접근하는 프로그램을 작성할 때
      • IDE에서 타입 확인과 자동 완성이 가능하기 때문
  • 자바스크립트, 루비, 파이썬 같은 동적 타입 프로그래밍 언어
    • 만족시킬 컴파일 시점의 타입 검사기가 없어서 코드를 생성하는 것이 중요하지 않다.?
    • 명시적 컴파일 단계가 없음
    • 동적 생성 스키마의 경우에 (DB 테이블에서 생성한 Avro 스키마) 코드 생성은 데이터를 가져오는 데 불필요한 장애물이다.
  • 그래서 Avro는 정적 타입 프로그래밍 언어를 위해 코드 생성을 선택적으로 제공한다.
  • 객체 컨테이너 파일이 있다면 Avro 라이브러리를 사용해 열어 JSON 파일을 보는 것처럼 데이터를 볼 수 있다.

스키마의 장점

  • 프로토콜 버퍼, 스리프트, 아브로 와 같은 스키마 언어는 XML 스키마나 JSON 스키마보다 훨씬 간단하며 더 자세한 유효성 검사 규칙을 지원한다.
  • 구현과 사용이 더 간단하므로 광범위한 프로그래밍 언어를 지원하는 방향으로 성장중이다.
  • 이진 부호화를 독자적으로 구현하는 시스템도 있는데
    • 대부분의 RDB에는 질의를 DB로 보내고 응답 받을 수 있는 네트워크 프로토콜이 있다.
    • 특정 DB에 특화되고 특정 DB 네트워크 프로토콜로부터 응답을 “인메모리 데이터 구조” 로 복호화하는 드라이버를 제공한다.
  • 필드이름을 생략가능해서 크기가 작을 수 있다.
  • 스키마 자체가 유용한 문서화 형식이다.
  • 스키마 DB를 유지하면 스키마 변경을 적용하기전에 상/하위 호환성을 확인할 수 있다
  • 정적 타입 프로그래밍 언어에서 스키마로부터 코드를 생성하는건 유용하다.
    • 컴파일 시점에 타입 체크를 할 수 있다.

데이터플로 모드

하나의 프로세스에서 다른 프로세스로 데이터를 전달하는 방법

데이터베이스를 통한 데이터플로

  • 데이터베이스에 기록하는 프로세스
    • 데이터를 인코딩
  • 데이터베이스에서 읽는 프로세스
    • 데이터를 디코딩

데이터를 접근하는 단일 프로세스가 있다면?

읽기는 단순히 동일 프로세스의 최신 버전이다.

 

데이터베이스에는 다양한 프로세스가 접근할 수 있기 때문에

데이터베이스 내 값이 “새로운 버전의 코드로 기록된 다음" 현재 수행 중인 “예전 버전의 코드로 그 값을 읽을" 가능성이 있음 → 그래서 데이터베이스에서의 상위 호환성도 필요하다.

 

레코드 스키마에 필드를 추가하고 새로운 코드는 새로운 필드를 위한 값을 데이터베이스에 기록하는 상황에서

예전 버전 코드 (새로운 필드를 모름)가 레코드를 읽고 생신한 후 갱신한 값을 다시 기록한다.

→ 예전 코드가 해석할 수 없더라도 새로운 필드를 유지하는게 바람직함.

 

다양한 시점에 기록된 다양한 값 Different values written at different times

데이터베이스는 값 갱신이 가능하므로 어떤 값은 5밀리초 전일지도, 5년전일지도

애플리케이션은 몇 분내에 새로운 버전 배포가 가능하지만 데이터베이스는 그렇지 않으므로

“명시적으로 다시 기록하지 않는한" 원래의 부호화 그대로 있다.

데이터가 코드보다 더 오래 산다. Data outlives code.

 

데이터를 새로운 스키마로 rewiriting하는 것은 대용량 데이터셋 대상으로 하기에는 너무 값비싼 작업이니

대부분의 관계형 데이터베이서는 널을 기본값으로 갖는 새로운 컬럼 추가 정도의 간단한 스키마 변경만 허용한다.

예전 로우를 읽는 경우 디스크 상의 부호화된 데이터에서 누락된 임의 컬럼은 널로 채운다.

 

보관 저장소 Archival storage

백업 목적이나 데이터 웨어하우스로 적재하기 위해 데이터베이스의 스냅숏을 수시로 만드는 경우

일관되게 부호화한 형태로 Avro 객체 컨테이너 파일과 같은 형식으로 데이터 덤프를 하면 된다.

왜냐하면 한 번에 기록하고 이후에는 변하지 않기 때문

서비스를 통한 데이터 플로 : REST 와 RPC

Dataflow Through Services: REST and RPC

API는 표준화된 프로토콜과 데이터 타입 HTTP, URL, SSL/TLS, HTML 등으로 구성되고

모두가 여기 동의하기에 우리는 웹 브라우저로 웹 사이트에 접근할 수 있다.

 

각각의 서버는 또 클라이언트로 동작할 수 있고 이런 식으로 하나의 서비스가 또 다른 서비스에 요청을 하는 개발방식을 예전에는

서비스 지향 설계 Service-Oriented Architecture SOA라고 불렀고

이를 요즘은 마이크로서비스 설계 Micorservices Architecture 라고 부른다.

 

마이크로서비스 체계에서는 각 서비스들이 독립적으로 관리되고 배포된다.

예전 버전과 새로운 버전의 서버와 클라이언트가 동시에 실행된다.

각각이 계속 버전이 달라진다는 뜻이고, 그러니 서버와 클라이언트의 인코딩은 서비스 API의 버전 간 호환이 가능해야 한다.

 

원격 프로시저 호출 RPC 문제

Remote Procedure Call RPC

RPC 모델은 원격 네트워크 서비스 요청을 같은 프로세스 안에서 특정 프로그래밍 언어의 함수나 메서드를 호출하는 것과 동일하게 사용 가능하게 해준다.

(마치 함수 쓰는것 처럼 같은 프로세스 안에서)

 

그러니까 서로 다른 환경에서 서비스 간 프로시저를 호출해서 언어나 이런 부분에 구애받지 않기 떄문에 MSA 환경에서 많이 쓰인다고 한다.

  • RPC의 구조를 좀 더 자세히 보기로 하자.

출처 : https://www.geeksforgeeks.org/remote-procedure-call-rpc-in-operating-system/

  • Python으로 간단하게 구현된 RPC 서버와 클라이언트 예제 코드
    • 둘다 Python이라 .. MSA가 와닿지는 않을 수 있음
    • 아래는 예제 링크
    • RPC
 

Python :Python with RPC services

RPC(Remote Procedure Call)—— a remote procedure call, which is a request for a service from a remote computer program over a network , you don't need to know the protocols of the underlying network technologies. RPC adoption client / server mode.

www.codestudyblog.com

 

RPC가 처음에는 편리한 것 같지만 RPC 접근 방식은 결함이 있는데 그건 바로 네트워크 요청이라는 것.

네트워크 문제로 요청과 응답이 유실되거나 원격장비가 느려지거나 요청에 응답하지 않거나.. 하는 문제는 일상적이기에 같이 고려를 해야한다. 

 

문제의 예시를 책에서 들어준다.

  • 네트워크 요청은 타임아웃으로 결과 없이 반환될 수 있다. 이 경우 무슨 일이 있었는지 알 수 없음
  • 실패한 네트워크 요청을 다시 시도할 때 실제로는 처리 되고 “응답만" 유실 될수도 있다.
  • 네트워크 요청은 함수 호출보다 훨씬 느리고 지연 시간은 다양하다. (예를들면 AWS 한국 리전에서 → 미국 리전의 자원을 호출하는 경우?)
  • 매개변수를 네트워크로 전송하기 때문에 바이트열로 부호화 해야 한다. 이게 크면 또 그만큼 비싼 작업이 될 수 있다.
  • 서로 다른 언어로 구현이 가능하지만 모든 언어가 같은 타입을 갖지는 않기 때문에 모양이 이상해질 수 있다.

RPC의 현재 방향

저런 문제점을 충분히 인지하고 있기 때문에 방향성은 아래와 같음.

  • 스리프트와 아브로는 PRC 지원 기능 내장
  • gRPC는 프로토컬 버퍼를 이용한 RPC 구현
  • 피네글은 스리프트 사용
  • Rest.li는 HTTP위에 JSON사용 (?)

위에 언급한 RPC의 문제 때문에 차세대 RPC 프레임워크는 원격 요청이 로컬 함수 호출과 다르다는 사실을 더욱 분명히 한다.

  • 피네글과 Rest.li
    • 실패할지 모를 비동기 작업을 캡슐화 하기 위해 Future Promise를 사용
    • Future는 병렬로 여러 서비스에 요청을 보내야 하는 상황을 간소화하고 요청 결과를 취합
  • gRPC는
    • 하나의 요청과 하나의 응답뿐만 아니라 시간에 따른 일련의 요청과 응답으로 구성된 스트림을 지원

이진 부호화 형식을 사용하는 사용자 정의 RPC 프로토콜이 우수한 성능을 제공할지라도
RESTful API는 고유한 장점이 있는데 실험과 디버깅에 적합 하다는 것이다.

데이터 부호화와 RPC의 발전

발전성이 있으려면 RPC 클라이언트와 서버를 독립적으로 변경하고 배포할 수 있어야 한다.

 

RPC 스키마의 상하위 호환 속성은 거기에 사용된 모든 부호화로부터 상속되는데 아래처럼 된다.

  • 스리프트, gRPC(프로토콜 버퍼), 아브로 RPC는 각 부호화 형식의 호환성 규칙에 따라 발전할 수 있다.
  • SOAP에서 요청과 응답은 XML 스키마로 지정된다. 이 방식은 발전 가능하지만 일부 미묘한 함정이 있다.
  • RESTful API는 응답에 JSON 을 가장 일반적으로 사용한다.

서비스 제공자는 보통 클라이언트를 제어할 수 없고, 강제로 업그레이드도 할 수 없다. (게임이 아니니까!)

안타깝게도 그러니까... 호환성은 무한정 오래 유지되어야 하고 서비스 제공자는 변경을 위해 여러 버전의 서비스 API를 함께 유지하기도 한다.

 

  • 각 특징이 정리된 그림

출처 : https://www.altexsoft.com/blog/soap-vs-rest-vs-graphql-vs-rpc/

메시지 전달 데이터플로

REST와 RPC는 하나의 프로세스가 네트워크를 통해 다른 프로세스로 요청을 전송하고 가능한 빠른 응답을 기대하는 방식이다.

그리고 데이터베이스는 하나의 프로세스가 부호화한 데이터를 기록하고 다른 프로세스가 언젠가 그 데이터를 다시 읽는 방식을 사용한다.

  • 비동기 메시지 전달 시스템 asynchronous message-passing system
    • 낮은 지연 시간으로 다른 프로세스로 전달한다는점에서는 RPC와 비슷하나,
    • 메시지를 직접 네트워크로 연결로 전송하지 않고
    • 임시로 메시지를 저장하는 메시지 브로커 message broker (또는 메시지 큐 message queue)
    • 메시지 지향 미들웨어 message-oriented middleware
    • 라는 중간 단계를 거쳐 전송한다는 점에서는 데이터베이스와 유사하다.

RPC와 비교해서 메시지 브로커의 장점

  • 수신자 recipient가 사용 불가능하거나 과부하 상태라면 메시지 브로커가 버퍼처럼 동작할 수 있기 때문에 시스템 안정성이 향상된다.
  • 죽었던 프로세스에 메시지를 다시 전달할 수 있기 때문에 메시지 유실을 방지할 수 있다.
  • 송신자 sender가 수신자의 IP 주소나 포트 번호를 알 필요가 없다.
  • 하나의 메시지를 여러 수신자로 전송할 수 있다.
  • 논리적으로 송신자는 수신자와 분리된다. pub-sub 구조라서

단방향 통신이라서 송신 프로세스는 대개 메시지에 대한 응답을 기대하지 않는다.

 

이런 통신 패턴이 비동기다. 송신 프로세스는 메시지가 전달될 때까지 기다리지 않고 단순히 메시지를 보낸 다음 잊는다.

메시지 브로커

  • 과거의 메시지 브로커 : 상용
    • 팁코, IBM 웹스피어, 웹메소즈
  • 최근의 메시지 브로커 : 오픈소스가 많은듯.
    • 래빗MQ, 액티브MQ, 호닛Q, 나츠, 아파치 카프카

어떻게 사용하냐면

  • 프로세스 하나가 메시지를 이름이 지정된 큐나 토픽으로 전송하고
  • 브로커는 해당 큐나 토픽 하나 이상의 소비자 consumer 또는 구독자 subscriber 에게 메시지를 전달한다.
  • 동일한 토픽에 여러 생산자 와 소비자가 있을 수 있다.
  • 토픽은 단방향 데이터 플로만 제공한다.
  • 하지만 소비자 스스로 메시지를 다른 토픽으로 게시하거나
    • 원본메시지의 송신자가 소비하는 응답 큐로 게시할 수 있다.
  • 특정 데이터 모델을 가용하지 않고, 메시지는 일부 메타데이터를 가진 바이트열이므로 모든 부호화 형식을 사용할 수 있다.

만약에 여기서 사용하는 부호화가 상하위 호환성을 모두 가진다면?

메시지 브로커에서 게시자와 소비자를 독립적으로 변경해서 배포순서도 임의로 변경하는 식으로 유연하게 운영이 가능하다.

 

Reference

반응형