본문 바로가기

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

[4] 데이터를 다른 시스템에 전송하기 위한 부호화 (인코딩) 와 호환성

부호화와 발전

: 데이터 부호화를 위한 다양한 형식을 살펴본다.
데이터 스키마가 바뀌면 상위호환, 하위호환은 어떻게 하는지 알 수 있다.
웹서비스나 메시지 큐에서 부호화 형식이 데이터 저장, 통신에 어떻게 쓰이는지 알 수 있다.

시스템은 호환성이 유지될 수 있어야한다.

※ 호환성: 이전 버전과 새 버전의 데이터가 공존해도 시스템은 정상적으로 돌아가는것
관계형 DB 같이 쓰기 시점에 스키마가 정해지는 쓰기 스키마와 다르게 읽기 스키마(schemaless) 는 스키마를 강요하지 않으므로 한 컬렉션 안에 새로운 버전과 이전 버전의 데이터타입이 섞여들어갈수있다.
근데 애플리케이션 단에서는 새 버전에 대한 대응 코드를 즉시 반영할 수 없다. 왜?

  • 서버 단: 대규모 시스템에서는 트래픽 처리를 위해 여러 개 서버군이 한 역할을 하는데 여기서 무중단 배포를 위해서 이전 버전 서버, 새 버전 서버가 공존할수밖에 없다.
    • (elsboo) 무중단 배포 전략 - 롤링, 블루/그린, 카나리(https://hudi.blog/zero-downtime-deployment/) (블루그린도 독립적인 환경이긴 하지만 배포 전 테스트는 해야하니 공존해야하는 상황은 존재한다)
  • 클라 단: 업데이트는 사용자의 선택에 좌우된다. 앱을 사용자가 업데이트 할수도있고 안할수도 있으니깐

그렇기 때문에 새로운것과 이전의 데이터 타입이 공존할 수 있도록 호환성을 잘 유지할 수 있어야 한다.
(elsboo) 예시) 사용자가 등록한 게시글을 DB 에 저장하는 프로그램이 있다. 좋아요 필드가 추가되었다.

  • 하위 호환성: (위 → 아래) 새 코드는 예전 코드가 기록한 데이터를 읽을 수 있어야 한다.
    • (elsboo) 좋아요 데이터가 없을 옛날 데이터도 정상적으로 읽어야한다.
  • 상위 호환성: (아래 → 위) 예전 코드는 새 코드가 기록한 데이터를 읽을 수 있어야 한다.
    • (elsboo) 좋아요 데이터가 있는 새 데이터도 예전 코드가 정상적으로 읽어야한다.

데이터 부호화 형식

※ 부호화: 다른 시스템에세도 데이터를 이해할 수 있도록 표현 방식을 바꿔주는거
원래 데이터는 어떻게 생겼길래 표현 방식을 바꿔줘야하는거지?
❗ 원래 데이터는 메모리에 저장되어있다. 객체, 배열, 해시테이블, 트리 등으로 데이터가 유지되어있고 cpu에서 호율적으로 접근하기 위해 보통 포인터로 구성된다. 근데 이런 포인터를 네트워크로 전송하면 딴 시스템은 못알아들을 것이므로 일련의 바이트열(ex. 1110101010) 로 변환해서 보내야 한다. (인메모리 표현 → 바이트열로 전환)
=> 메모리를 공유하지 않는 다른 프로세스로 데이터를 보내고 싶을 때 바이트열로 부호화해서 보내야함
어떤 표현 방식이 있지?
❗ JSON, XML, 프로토콜 버퍼, 스리프트(Thrift), 아브로(Avro)

용어정리

인메모리 -> 바이트열: 부호화(직렬화, 마샬링)

  • (elsboo) 직렬화: 자바 객체를 바이트로 / 마샬링: Object를 XML string 으로
    바이트열 -> 인메모리: 복호화(파싱, 역직렬화, 언마샬링)
  • (elsboo) 역직렬화: 바이트를 자바 객체로 / 언마샬링: XML string을 Java Object로

언어별(자바, 파이썬, 루비 등)로 부호화 기능을 제공하지만 문제점이 많다

  • 해당 언어에 의존적이기 때문에 그 언어로만 작성해야하고 다른 언어를 사용하는 시스템과 통합하는데 방해가 된다.
  • 보안 리스크 - 공격자가 바이트열을 복호화하게 되면 임의의 클래스를 인스턴스화하수있고 그럼 임의의 코드도 실행할 수 잇다.
  • 버전관리가 잘 안돼서 상위 호환 하위 호환할 때 불편한가보다
  • 효율성(부호화, 복호화 하는데 걸리는 시간, 부호화된 구조체 크기) 도 별론가 보다. 자바 직렬화도 느리고 사이즈가 커진다 한다.

JSON, XML, CSV 는 human readable 이지만 미묘한 단점

  • 숫자 타입 표현 어려움. xml, csv 는 못하고 json 은 숫자와 문자열을 구분할순있지만 정밀한 소수점은 표현하기 어렵다.
  • 이진 문자열 (문자부호화 없는 바이트열) 을 지원하지 않아 이진데이터를 Base64를 사용해 텍스트를 부호화해 이런 제한을 피한다. Base64로 부호화됐기 때문에 해석해줘야한다. ???
  • XML, JSON 은 스키마를 지원하지만 강제하진않고 스키마 정보에 따라 올바른 해석이 다르기 때문에 JSON, XML 스키마를 사용하지 않은 애플리케이션은 하드코딩으로 부호화/복호화 작업을 해줘야한다.
  • csv 는 스키마가 없어서 애플리케이션단에서 명시해야하고 컬럼 변경되면 수동으로 코드도 바꿔줘야하고 쉼표나 개행 처리가 모호하다. 처리 규칙은 있지만 모든 파서가 지키지는 않는다.
    => 이렇게 단점이 있어도 타 조직 간의 데이터 교환 형식으로 충분하기 때문에 많이 쓰일것이다.

이진부호화

(elsboo) ? 이진부호화는 데이터를 0,1로 부호화한다는 뜻인가
JSON, XML 은 이진 형식에 비해 훨씬 많은 공간을 차지한다.
JSON 용 이진 부호화(메시지팩, BSON 등) 방식이 있지만 JSON 텍스트 만큼 널리 채택되진 않았다.그리고 데이터 모델은 유지 하기 때문에 객체의 모든 필드 이름도 포함해야 한다.

부호화할 예제 json text

 

json

messagePack 으로 인코딩하면: 66byte

messagePack: JSON용 이진 부호화 형식

MessagePack encoding

필드명, 값, 문자열 길이 정보, 데이터 타입 정보 포함

https://moo-you.tistory.com/329(10진수 / 16진수 / 2진수 변환표)

Apache Thrift(페이스북), Protocol buffer(구글)

같은 원리로 만들어진 이진 부호화 라이브러리
둘 다 스키마가 따로 필요하다

Thrift IDL
Protocol Buffer IDL

 

스리프트: 바이너리 프로토콜(59byte), 컴팩트 프로토콜(34byte)

바이너리 프로토콜(59byte)

필드명데이터가 빠져 줄음, 대신 필드 태그 정보, 데이터 타입 정보, 길이 정보, 데이터
Binary Protocol encoding

컴팩트 프로토콜(34byte)

길이정보나 정수값은 가변길이로 부호화해 줄임, 필드 태그,  데이터타입 합쳐서 1바이트로

Compact Protocol encoding

 

프로토콜 버퍼 (33byte)

protocol buffer encoding

컴팩트 프로토콜과 같고 구성의 끝 1바이트만 빠짐
 

필드 추가/변경/삭제 됐을 때 호환성을 유지하는 방법

※ 스키마 발전(schema evoluation): 스키마가 시간에 따라 변하는 것

스리프트, 프로토콜 버퍼

이 둘의 특징은 필드명이 필드 태그 숫자로 연결돼있다.
그래서 필드명은 맘껏 수정 가능.

필드 추가/삭제

필드 추가는 새로운 태그 숫자 부여
- 새로 추가할 땐 optional 이거나 required 하려면 기본값가져야함
삭제할 땐 optional 필드만 삭제 가능. 한번 사용한 필드 태그 번호는 다시 사용 못함(이전 버전은 태그번호 계속 사용할 수 있기 때문)
-상위 호환 유지하는 법: 새로 추가된 필드 무시 가능. 데이터타입 주석은 파서가 몇바이트 건너뛸수 있는지 알려준다. (길이 바이트 말하는건가?)
-하위 호환 유지하는 법: 상위 버전에서도 원래 있던 데이터는 계속 같은 의미를 가지기 때문에 가능.

데이터타입 변경

불가능하진 않지만 숫자인 경우는 잘릴 수 있다.
- ex) 32bit -> 64bit 정수 로 바꿈. 새 버전 코드에서 데이터를 쓰면 64bit 꽉채울거고 이전 버전에선 32bit 만 알아들을것이므로 일부는 잘릴 것

리스트와 단일 필드간의 변경

프로토콜 버퍼: 쉽다. repeated 옵션이 동일 필드를 여러 번 나타내는 리스트 타입인데, 만약 단일(optional) 에서 다중(repeated) 로 변경했다 치면
  • 상위 호환: 예전 버전에서 새 데이터를 읽을 수 있어햐 함 -> 단일 필드이니 마지막 요소만 보면 됨  
  • 하위 호환: 새 코드는 예전 데이터를 읽을 수 있어야함 -> 새 코드는 리스트이므로 0개나 1개로 간주하여 읽으면 됨
스리프트: 전용 목록 datatype 이 있는데 단일값에서 다중 값으로의 변환은 지원하지 않는다. 대신 중첩된 목록은 지원한다.
 

아브로(Apache Avro)

출현 배경: 아파치 스리프트가 하둡 방식에 맞지 않아 하둡용으로 새로 만듦
두개 스키마가 있다.
  • IDL(Avro IDL) : 사람이 편집하는
  • JSON기반 : 기계용
Avro IDL, json

아브로 32byte

Avro encoding
필드, 데이터타입 식별자 없음- 대신 IDL 순서대로 데이터를 기입해 미리 파악해야함., 데이터는 가변길이 부호화

스키마 변경(발전)은 어떻게 지원?

쓰기스키마는 데이터 쓸 때 정한 스키마고 읽기 스키마는 데이터 읽을때 필요한 스키마이다.
아브로의 핵심 아이디어는 쓰기와 읽기 스키마가 반드시 일치할 필요는 없고 호환가능하기만 하면 된다.
스키마 해석시 쓰기와 읽기 스키마를 필드명으로 매핑하기 때문에 필드 순서가 일치할 필요는 없다.
읽기에 없는데 쓰기에만 있는 필드다? 무시
쓰기엔 없는데 읽기에만 있는 필드다? 디폴트 값으로 읽음

writer schema & reader schema

스키마 발전하려할 때 지켜야 하는 규칙

상위 호환성: 새 버전의 쓰기가 옛날 버전의 읽기를 가질 수 있어야함
하위 호환성: 옛 버전의 쓰기가 새 버전의 읽기를 가질 수 있어야함
 
-기본값 있는 필드만 추가나 삭제 가능 => 그래야 상위 호환 시 옛 데이터를 읽을 수 있음(기본값으로).
-null 을 활용하려면 union type 이어야함
  • union {null, long} field 처럼 여러 data type 을 가지는 타입
-프로토콜버퍼 처럼 optional, required 는 없음
-data type 변경 가능, 필드명 변경 가능(alias 있어서 읽기 필드명 alias를 예전 쓰기필드명과 매칭시키면 됨)

읽기 입장에서는 어떤 쓰기 스키마로 데이터를 부호화했는지 알 수 있을까?

상황1 동일한 스키마, 대용량 레코드
: 시작 부분에 한 번만 스키 스키마 포함
 
상황2 개별적으로 다른 스키마
: 스키마를 버저닝해서 레코드마다 스키마 버전을 명시
 
상황3 네트워크 연결을 통할 때
: 양방향으로 스키마 버전을 합의. 연결이 지속되는 동안 합의된 스키마 사용 (RPC 프로토콜)

아브로 장점: 동적 생성 스키마

RDB 데이터를 아브로를 사용해 부호화한다고 해보자. RDB 컬럼이 추가되고 삭제되도 아브로가 알아서 인지하고 새로운 스키마를 생성하므로 우리는 아브로 스키마를 수동으로 조작할 필요가 없다.
프로토콜 버퍼, 스리프트는 수동으로 동기화해줘야하는데 말이다.
 
그래서 자바스크립트, 루비, 파이썬 같은 동적 타입 플밍 언어에서는 아브로가 적합하고,
자바, C++, C#같은 정적 타입 언어에서는 프로토콜 버퍼, 스리프트가 유용하다(아브로도 정적 타입을 제공하긴 함)

스키마가 있으면 좋은 점

-스키마가 있는 프로토콜 버퍼, 스리프트, 아브로의 좋은 점은 XML이나 JSON보다 간단하고 정규식이나 값 제한 같은 유효성 검사 규칙을 지원한다.
-부호화 데이터에 필드를 생략할 수 있어 이진 JSON 변형보다 크기가 작다.
-스키마는 데이터와 동기화되어있기때문에 수동으로 문서 관리하는것보다 유용한 문서화 형식이다.
-스키마 변경 전에 상위/하위 호환성을 확인할 수 있다.
-자바 같은 정적 타입 언어에서 스키마로부터 타입을 결정하면 컴파일 타임에 에러 안나게 잡을 수 있다.

다른 프로세스간의 데이터를 전달하는 방법 3가지

: DB 를 통해, 서비스 호출(REST, RPC)을 통해, 비동기 메시지 전달을 통해

1. DB 를 통해

: DB 기록하는 프로세스는 데이터를 부호화하고, 읽는 프로세스는 복호화한다.
 
이런 경우 조심하기: 필드 추가된 새 버전 데이터를 예전 코드가 읽고 다시 썼을 때 새버전의 추가된 필드가 유실될 가능성이 있다. 
read new data with old code

스키마 발전

스키마가 발전할 땐 새로운 스키마로 데이터 마이그레이션을 하는 방법이 있지만 리소스가 커서 잘 안한다.
대신 기본값 null 인 컬럼 추가 정도는 허용한다. 예전 레코드는 누락된 컬럼 값을 null 로 채우고말이다.
그래서 스키마 버전이 아니라 스키마가 변경돼도 단일 스키마로 부호화하는 것처럼 보이게 한다.

2. 서비스 호출을 통해 (REST, RPC)

: 데이터를 요청하는 클라와 데이터를 제공하는 서버로 역할을 나눈다.
 
DB를 통해 제공하는 것과의 차이점은 DB 는 쿼리로 데이터를 마음껏 가져갈 수 있는데 서비스는 API 를 통해 정해진 데이터만 가져갈 수 있게 캡슐화가 되어있다.
 
HTTP 프로토콜로 통신하는 서비스를 제공하는 방법으로 REST, SOAP 가 있다.
(elsboo) SOAP 장단점(링크)
 
RPC(Remote Procedure Call, RPC) 모델
  • 원격 네트워크 서비스 요청을 특정 함수나 메소드 호출하는 것과 동일하게 사용가능하게 해주는것
  • 네트워크 통신은 예측이 어렵기 때문에 다음을 고려해야 한다.

3. 메시지 전달을 통해

메시지를 임시로 저장하는 메시지 브로커가(메시지 큐) 중간에 낀다. 이렇게하면 장점이
  • 수신자가 사용 불가능할 경우 메시지를 저장하는 버퍼처럼 동작하기 때문에 시스템 안정성이 향상된다.
  • 송신자가 수신자에 대해 알 필요가 없다.
  • 하나의 메시지를 여러 수신자에게 전송할 수 있다.
  • 논리적으로 송신자와 수신자가 분리돼서 publisher는 consumer 가 누군지 신경쓰지 않아도 된다.

구조

단방향이다.
송신 프로세스는 메시지 응답을 대개 기대하지 않는다. (비동기로 동작)
메시지는 바이트열이므로 모든 부호화 형식을 사용할 수 있고, 그래서 특정 데이터 모델을 강요하지 않는다.
 

PART2 분산 데이터

지금까지의 데이터 부호화는 단일 장비일 때 이다. 여러 장비일땐 어떻게 할까?

일단 여러 장비가 필요한 이유

  • 부하가 커졌을 때 장비를 추가해 확장성을 확보할 수 있다.
  • 서버 하나가 실패해도 다른 하나가 이어받는 내결함성/고가용성을 보장할 수 있다.
  • 세계 여러 곳곳에 서버가 존재하면 사용자 지역 근처의 서버와 통신하면 되므로 지연시간을 줄일 수 있다.

데이터 분산하는 방법 2가지

  • 복제(리플리카): 복사본을 다른 노드에 위치시켜 한 노드에 장애났을 때 다른 노드의 복사본으로 서빙할 수 있다. 성능향상에도 도움된다는데 5장에서 알아보겠다.
  • 파티셔닝(샤딩): 큰 데이터를 작은 단위의 파티션으로 나누고 다른 노드에 각각 저장한다. 6장에서 알아보겠다.

partition & replica

 

레퍼런스

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