이전 장
데이터 부호화, 왜 알아야 하는가
데이터부호화 형식에 따라 human-readable(사람이 읽기에 좋은), 데이터 압축률(가용성 증가, 통신 성능 증가), 호환성 유지에 영향을 줄 수 있다.
데이터 부호화란
어플리케이션에서는 메모리에 데이터를 저장할 때 객체, 구조체, 배열 등의 구조로 저장한다. 보통 CPU가 효율적으로 접근할 수 있도록 ‘포인터'를 사용하는데, 이는 다른 언어를 사용하는 프로세스에서는 이해하지 못할 수 있다. 그러므로 데이터를 파일에 쓰거나 네트워크로 전송하려면 바이트 형태로 저장한다.
•
부호화: 인메모리 → 바이트 (직렬화, 마샬링)
•
복호화: 바이트 → 인메모리 (역질렬화, 언마샬링, 파싱)
정리하면, 데이터 부호화는 통신 주체(프로세스 또는 서버)가 다른 언어의 데이터 구조를 사용해도 상대 프로세스가 이해할 수 있도록 데이터를 전환하는 것을 말한다. 더 쉽게 말하면 어플리케이션 환경에 구애받지 않는 데이터 형식으로 전환하는 과정을 말한다.
호환성 유지
어플리케이션은 계속 변경된다. 그에 따라 데이터 모델도 변경된다. 데이터 모델을 변경하는 과정에서 어플리케이션이 중지되지 않고 계속 돌아가기 위해서는 상위호환성과 하위호환성을 지켜야 한다.
•
하위호환성: 새로운 코드가 이전 코드에서 기록한 데이터를 읽을 수 있어야 함
•
상위호환성: 이전 코드가 새로운 코드에서 기록한 데이터를 읽을 수 있어야 함(새로 추가된 필드는 못읽겠지만 적어도 읽는 데 오류가 없어야 함)
JSON
JSON같은 경우는 스키마를 잘 사용하지 않는다. 데이터를 추가하고 제거하는 게 자유롭기 때문에 호환성을 지키기 위해서는 히스토리를 잘 알아야 한다. 찾아보니 스키마가 있긴 하다. 이 레포를 보면 json schema를 통해 json 데이터를 검증한다. json schema 만드는 법은 json schema 문서를 참고하면 된다. json schema도 json으로 되어있는 게 함정이다ㅋㅋ
// json-kotlin-schema 코드
val schema = JSONSchema.parseFile("/path/to/example.schema.json")
val json = File("/path/to/example.json").readText()
require(schema.validate(json))
Kotlin
복사
thrift, protocolbuffer
thrift, protocolbuffer(줄여서 protobuf)같은 경우는 비슷하게 생겼다. 둘 다 RPC(remote process call) 프레임워크이다. thrift는 struct, protobuf는 message로 인터페이스를 정의한다.
이렇게 인터페이스로 스키마를 정의하는 언어를 IDL이라고 한다. 이 스키마는 어플리케이션 서버를 구현하는 언어의 타입, 구조체로 재정의 되도록 지원하는 제네레이터가 있는 것 같다(안찾아봐서 확실치는 않다). 마치 typescript-graphql-generator처럼?
아무튼 thrift와 protobuf 둘 다 인덱스가 붙어있는 걸 볼 수 있는데 이 때문에 필드 중간에 새 필드를 추가하거나 기존 필드를 삭제하면 호환성에 문제가 생길 수도 있다. 그럼 굳이 왜 인덱스를 붙이느냐? 인덱스를 부여함으로써 성능을 높일 수 있다.
thrift
struct Person {
1: required string UserName,
2: optional i64 favoriteNumber,
}
C
복사
protocolbuffer
message Person {
required string user_name = 1;
optional i64 favorite_number = 2;
}
C
복사
가독성
JSON, XML같은 경우 사람이 알아보기 쉬운 데이터 구조이다. 반면 데이터를 압축하기 위해 이진 부호화를 하면 사람이 알아볼 수 없는 상태가 된다. 만약 데이터 자체가 크지 않거나, 이진 부호화를 했을 때의 압축률이 가독성을 트레이드오프하기에 아주 미미하다면 가독성을 택하는 게 나을 수도 있겠다.
성능(데이터 압축률)
JSON
빈 공간이 많고 데이터를 그대로 유지하기 때문에 데이터 크기가 크다. JSON을 이진부호화해서 용량을 줄이는 MessagePack(메시지팩)도 있다. 메시지팩은 각 필드마다 타입, 데이터 길이, 값을 바이트로 변환한다. 이렇게 빈 공간을 절약해서 파싱 속도를 높일 수 있다. (하지만 효과는 미미하다)
thrift
바이너리 프로토콜, 컴팩트 프로토콜 두 가지 이진 부호화 방식을 가진다.
thrift 스키마를 보면 필드에 인덱스가 부여된 것을 볼 수 있다. 메시지팩 같은 경우는 필드 이름 또한 그대로 바이트로 변환하여 데이터 크기를 줄이는 데 별 효과가 없었다.
하지만 thrift의 경우 필드 이름을 인덱스로 대체할 수 있게 되어 데이터 크기를 줄이는 데 효과적이다.
바이너리 프로토콜의 경우 각 필드는 타입, 필드 태그(인덱스), 데이터 길이, 값의 바이트로 되어있다.
struct Person {
1: required string UserName,
2: optional i64 favoriteNumber,
}
C
복사
바이너리 프로토콜 | 출처: 데이터중심어플리케이션 설계
컴팩트 프로토콜의 경우는 데이터 크기를 더 줄일 수 있다. 타입과 필드 태그를 합쳐 1바이트로 나타내고, 숫자는 8바이트를 모두 사용하는 대신, 이진수로 변환하여 2바이트로 줄인다. 바이너리 프로토콜보다 압축률이 더 높다.
컴팩트 프로토콜 | 출처: 데이터중심어플리케이션 설계
protocolbuffer
비트를 줄이고 저장하는 처리 방식은 약간 다르지만 부호화된 데이터를 보면 컴팩트 프로토콜과 비슷하다. 압축률도 비슷하지만 프로토콜 버퍼가 조금 더 성능이 좋은 듯 하다.
압축률
•
메시지팩: 81 → 66 바이트
•
바이너리 프로토콜: 81 → 59 바이트
•
콤팩트 프로토콜: 81 → 34 바이트
•
프로토콜 버퍼: 81 → 33 바이트
thrift와 protocolbuffer
thrift는 페이스북에서 개발, protocolbuffer는 구글에서 개발한 오픈소스이다. 둘 다 RPC 통신에 사용하는 프레임워크이다. thrift가 지원하는 언어가 더 많다. protocolbuffer의 경우 grpc에서 사용되고 있으며 압축률이 좋아 성능을 높일 수 있다. thrift 역시 컴팩트 프로토콜의 경우 protocolbuffer와 압축률이 비슷한 듯 한데(프로토콜 버퍼가 좀 더 좋은 듯), protocolbuffer는 grpc에서 사용되고 있어서 상대적으로 덜 알려진 게 아닌가 싶다(사실 잘 모름).
references
•
책 데이터중심어플리케이션설계 4장
•
https://blog.banksalad.com/tech/production-ready-grpc-in-golang/#뱅크샐러드의-grpc-패턴 뱅샐의 grpc 도입. 아직 읽어보진 않았지만 같이 읽으면 좋을 것 같다.