Table of Contents
애플리케이션은 계속해서 발전하고, 동일한 애플리케이션 간에도 짧든 길든 버전 차이가 발생하게 된다.
이렇게 발생하는 버전의 차이는 동일한 애플리케이션이든 클라이언트 서버 관계이든 호환성의 문제가 발생한다.
여기서는 대표적인 데이터 부호화 형식들에 대해 간단하게 알아보고 비교해본다.
이런 데이터 부호화 형식들에서 호환성의 문제에 대해서 알아본자.
또한 프로세스간에 데이터를 전달하는 보편적인 방법에 대해서도 살펴보자.
데이터 부호화 형식
프로그램은 보통(최소) 두 가지 형태로 표현된 데이터를 사용해 동작
- 메모리에 데이터 유지
객체, 구조체, 목록, 배열 등
CPU에서 효율적으로 접근 및 조작에 최적화 - 일련의 바이트열 형태로 부호화
파일에 쓰거나 네트워크를 통해 전송하기 위함
보통 메모리에서 사용하는 데이터 구조와 상당히 다름
두 가지 표현 사이에 전환
- 부호화
인메모리 표현에서 바이트열로 전환
직렬화, 마샬링이라고도 함 - 복호화
바이트열에서 인메모리 표현으로 전환
파싱, 역직렬화, 언마샬링
언어별 형식
많은 프로그래밍 언어는 인메모리 객체를 바이트열로 부호화하는 기능을 내장한다.
이는 최소한의 추가 코드로 인메모리 객체를 저장하고 복원할 수 있기 때문에 매우 편리하다.
하지만 심각한 문제점 또한 많다.
문제점
- 부호화는 보통 특정 프로그래밍 언어에 의존적
다른 언어에서 데이터를 읽기가 매우 어려움 - 보안 문제의 원인이 되기도 함
복호화 과정이 임의의 클래스를 인스턴스화할 수 있어야 하기 때문 - 데이터 버전관리를 잘 고려하지 않게 됨
- 효율성도 잘 고려하지 않게 됨
JSON과 XML 그리고 이진 변경
JSON, XML, CSV는 텍스트 형식이라 어느 정도 가독성이 있다.
하지만 피상적인 문법적 문제 외에도 일부 미묘한 문제가 있다.
문제점
- 수의 부호화의 애매함
XML과 CSV는 수와 숫자로 구성된 문자열을 구분할 수 없음
JSON은 문자열과 수를 구분하지만 정수와 부동소수점 수를 구분하지 않고 정밀도를 지정하지 않음 - 애매함으로 큰 수를 다룰 때 문제
- 이진 문자열을 지원하지 않음
Base64를 사용해 텍스트로 부호화해 이런 제한을 피함 -> 데이터 크기 33% 증가 - XML과 JSON 모두 스키마를 지원하지만 러닝커브가 큼
- XML과 JSON 스키마를 사용하지 않는 애플리케이션은 부호화/복호화 로직을 하드코딩해야 할 가능성 존재
- CSV는 스키마가 없음
로우와 컬럼의 의미를 정의하는 작업을 애플리케이션이 담당
이런 결점에도 JSON, XML, CSV는 다양한 용도에 사용하기에 충분하다
특히 데이터 교환 형식으로 사용하기에 매우 좋다.
그렇다고 읽기 쉽고 효율적이 형식이란 것은 아니다.
이진 부호화
1
2
3
4
5
6
7
8
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": [
"daydreaming",
"hacking"
]
}
이번 장에서 다양한 형식으로 이진 부호화할 레코드 예
JSON용 이진 부호화 형식인 메시지팩을 살펴보자
메시지팩 예제
위의 그림은 위의 JSON 문서를 메시지팩으로 부호화한 바이트열이다.
이진 부호화의 길이는 66바이트로 텍스트 JSON 부호화로 얻은 81바이트보다 작다.
JSON의 모든 이진 부호화는 이와 비슷하다.
이 같은 작은 공간의 절약이 사람의 가독성을 해칠 만큼 가치가 있는지는 확실치 않다.
스리프트와 프로토콜 버퍼
아파치 스리프트와 프로토콜 버퍼는 같은 원리를 기반으로 한 부호화 라이브러리다.
모두 스키마를 사용한다.
스리프트
인터페이스 정의 언어(interface definition language, IDL)로 스키마를 정의해야 한다.
1
2
3
4
5
struct Person {
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}
스리프트의 두 가지 다른 이진 부호화 형식
- 바이너리프로토콜(BinaryProtocol)
- 컴팩트프로토콜(CompactProtocol)
먼저 바이너티프로토콜을 살펴보자
스리프트의 바이너리프로토콜을 사용해 부호화 예제
메시지팩에서 필드 이름(username, favoriteNumber, interests)를 사용하던 것과 달리 필드 태그(1, 2, 3)을 사용한다.
이제 컴팩트프로토콜을 살펴보자
스리프트의 컴팩트프로토콜을 사용해 부호화 예제
- 의미상 바이너리프로토콜과 동일
- 필드 타입과 태그 숫자를 단일 바이트로 줄임
- 가변 길이 정수를 사용
1바이트 : -64~63
2바이트 : -8192~8191
…
동일한 정보를 34바이트로 부호화
프로토콜 버퍼
프로토콜 버퍼로 정의한 동일한 내용의 스키마이며 스리프트 스키마와 매우 비슷하다.
1
2
3
4
5
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
}
스리프트 컴팩트프로토콜과 부호화 형식도 매우 비슷하다.
프로토콜 버퍼를 사용해 부호화한 예제
스키마에 각 필드에는 required, optional 표시가 있지만 부호화하는 방법에서 차이가 나지 않는다.
하지만 required는 필드가 설정되지 않은 경우를 실행 시에 확인할 수 있어 버그를 잡을 때 유용하다.
필드 태그와 스키마 발전
스키마는 필연적으로 시간이 지남에 따라 변한다. 이를 스키마(schema evolution) 이라고 부른다.
스리프트와 프로토콜 버퍼에서 하위 호환성과 상위 호환성을 유지하면서 필드 태그에서 스키마 발전을 하는 것을 살펴본다.
- 스키마에서 필드 이름은 변경 가능하지만, 필드 태그를 변경할 수 없다.
- 새로운 필드 추가
이전 버전 코드에서 새로운 코드로 기록한 데이터를 읽는 경우, 해당 필드를 무시
추가되는 모든 필드는 optional 또는 기본값이 필요 - 필드를 삭제
optional 필드만 삭제 가능
같은 태그 번호는 절대 사용할 수 없음
데이터타입과 스키마 발전
스리프트와 프로토콜 버퍼에서 하위 호환성과 상위 호환성을 유지하면서 데이터타입에서 스키마 발전을 하는 것을 살펴본다.
- 데이터 타입 변경은 가능하지만, 값이 정확하지 않거나 잘릴 위험이 존재
- 프로토콜 버퍼에서 optional 필드를 repeated 필드로 변경 가능
- 이전 데이터를 읽는 새로운 코드는 0이나 1개의 엘리먼트가 있는 목록으로 인식
- 새로운 데이터를 읽는 이전 코드는 목록의 마지막 엘리먼트만 인식
- 스리프트는 repeated는 없지만 전용 데이터타입이 존재
위의 내용을 지원하지 않지만, 중첩된 목록을 지원한다는 장점이 있음
아브로
스리프트가 하둡의 사용 사례에 적합하지 않아 2009년 하둡의 하위 프로젝트로 시작했다.
아브로 또한 스키마를 사용하며, 2가지 스키마 언어가 있다.
- 아브로 IDL(Avro IDL)
사람이 편집할 수 있다.
1
2
3
4
5
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
- JSON 기반 언어
기계가 더 쉽게 읽을 수 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
{ "type": "record", "name": "Person", "fields": [ { "name": "userName", "type": "string" }, { "name": "favoriteNumber", "type": [ "null", "long" ], "default": null }, { "name": "interests", "type": { "type": "array", "items": "string" } } ] }
특징
- 스키마에 태그 번호가 없음
- 모든 부호화 중 길이가 가장 짧음
- 필드나 데이터타입을 식별하기 위한 정보가 없음
- 부호화는 단순히 연결된 값으로 구성
- 정수는 가변 길이 부호화를 사용해서 부호화
- 정확히 같은 스키마를 사용하는 경우에만 올바르게 복호화
아브로를 이용해 부호화한 예제
쓰기 스키마와 읽기 스키마
쓰기 스키마(reader’s schema) : 데이터를 아브로로 부호화할 때 사용하는 스키마
읽기 스키마(writer’s schema) : 이진 데이터를 복호화할 때 사용하는 스키마
아브로의 핵심 아이디어 : 쓰기 스키마와 읽기 스키마가 동일하지 않아도 되며 단지 호환 가능하면 된다
- 데이터 복호화할 때
아브로 라이브러리는 쓰기 스키마와 읽기 스키마를 함께 살펴본 다음 쓰기 스키마에서 읽기 스키마로 데이터를 변환해 그 차이를 해소
이를 스키마 해석(schema resolution)이라 함 - 스키마 해석
읽기 스키마에 없고 쓰기 스키마에 존재하는 필드는 무시
읽기 스키마에 있고 쓰기 스키마에 없다면 읽기 스키마의 기본값으로 설정
스키마 발전 규칙
아브로에서 상/하위 호환성의 의미
- 상위 호환성 : 새로운 버전의 쓰기 스키마와 예전 버전의 읽기 스키마를 가질 수 있음
- 하위 호환성 : 새로운 버전의 읽기 스키마와 예전 버전의 쓰기 스키마를 가질 수 있음
호환성 규칙
- 호환성을 유지하기 위해서는 기본값이 있는 필드만 추가 및 삭제
- 기본값이 없는 필드를 추가는 하위 호환성이 깨짐
- 기본값이 없는 필드를 삭제는 상위 호환성이 깨짐
- 아브로는 null을 기본값으로 허용하지 않음(버그를 막는데 도움)
유니온 타입을 사용해서 null을 허용할 수 있음 - 필드의 데이터타입 변경 가능
타입을 변환할 수 있기 때문 - 필드 이름 변경도 가능하나 까다로움
읽기 스키마는 필드 이름의 별칭을 부여 가능해서 별칭에 예전 쓰기 스키마 필드 이름을 매치할 수 있음
즉, 하위 호환성이 있지만 상위 호환성이 없음 - 비슷하게 유니온 타입에 엘리멘트를 추가하는 것은 하위 호환성은 있지만 상위 호환성은 없다.
쓰기 스키마란?
아브로는 이진 데이터를 복호화하기 위해서 자신의 읽기 스키마와 부호화에 사용된 쓰기 스키마를 사용해서 스키마 해석을 통해 복호화를 한다.
즉, 리더(복호화 하는 주체)는 쓰기 스키마가 필요하다.
이진 데이터를 보낼때 마다 쓰기 스키마를 포함 시킨다면 이진 데이터보다 스키마가 클 가능성이 있다.
그러면 이진 부호화로 절약한 공간이 의미가 없어진다.
아브로는 아래와 같이 해결을 했다.
- 많은 레코드가 있는 대용량 파일
파일의 시작 부분에 한 번만 쓰기 스키마를 포함 - 개별적으로 기록된 레코드를 가진 데이터베이스
데이터베이스의 다양한 레코드들은 다양한 쓰기 스키마를 사용해 서로 다른 시점에 쓰여질 수 있다.
가장 간단한 방법으로 모든 부호화된 레코드의 시작 부분에 버전 번호를 포함하고 데이터베이스에 스키마 버전 목록을 관리한다.
리더는 레코드를 가져와 버전 번호를 추출 후 데이터베이스에서 버전 번호에 해당하는 쓰기 스키마를 가져온다. - 네트워크 연결을 통해 레코드 보내기
통신할 때 연결 설정에서 스키마 버전 합의
이후 연결을 유지하는 동안 합의된 스키마를 사용
아브로 RPC 프로토콜의 동작
동적 생성 스키마
프로토콜 버퍼와 스리프트 대비 아브로 방식은 한 가지 장점이 있다.
스키마에 태그 번호가 없다는 점이다.
이 차이는 아브로가 동적 생성 스키마에 더 친숙하다는 점에서 온다.
예를 들어 관계형 스키마를 아브로 스키마로 쉽게 생성 가능하며 이 스키마를 이용해 레코드들을 부호화하고 아브로 객체 컨테이너 파일로 모두 덤프할 수 있다.
아브로 객체 컨테이너 파일에는 데이터베이스 스키마로 생성한 아브로 스키마를 포함한다.
그래서 스키마 변경에 신경 쓸 필요가 없다.
이에 반해 스리프트나 프로토콜 버퍼로 이런 용도로 사용한다면 필드 태그를 수동으로 할당해야 한다.
아브로는 동적 생성 스키마를 고려한 설계인 반면에 스리프트와 프로토콜 버퍼의 목표는 아닌였을 뿐이다.
코드 생성과 동적 타입 언어
스리프트와 프로토콜 버퍼는 코드 생성에 의존한다.
정적 타입 언어와 동적 타입 언어에서 코드 생성 관점에서 살펴보자
정적 타입 언어
- 효율적인 인메모리 구조 사용
- 데이터 구조에 접근하는 프로그램 작성 시 유용
타입 확인 및 자동완성이 가능해짐
동적 타입 언어
- 컴파일 시점의 타입 검사기가 없음
코드 생성이 중요치 않음 - 동적 생성 스키마의 경우 코드 생성은 데이터를 가져오는 데 불필요한 장애물
동적 생성 스키마의 예로 아브로
아브로는 코드 생성을 선택적으로 제공 한다.
(쓰기 스키마를 포함한)객체 컨테이너 파일이 있다면 코드 생성 없이도 JSON 파일을 보는 것과 같이 데이터를 볼 수 있다.
객체 컨테이너 파일
- 동적 타입 데이터 처리 언어(아파치 피그)와 함께 사용할 때 특히 유용
- 스키마를 생각하지 않고도 출력 파일에 파생 데이터를 기록할 수 있음
스키마의 장점
지금까지 살펴본 프로토콜 버퍼와 스리프트, 아브로는 스키마를 사용한다.
이 스키마 언어는 XML, JSON 스키마 대비
- 훨씬 간단
- 더 자세한 유효성 검사 규칙을 지원
- 구현과 사용이 더 간단
이진 부호화의 좋은 속성
- 부호화된 데이터에서 필드 이름 생략 가능
크기가 작아짐 - 스키마 자체로 유용한 문서화 형식
- 스키마 데이터베이스를 유지하면 스키마 변경 전에 상위/하위 호환성을 확인 가능
- 정적 타입 프로그래밍 언어에서 코드 생성은 유용(위에 참고)
데이터플로 모드
데이터플로는 매우 추상적인 개념이다.
하나의 프로세스에서 다른 프로세스로 데이터를 전달하는 방법이다.
데이터를 전달하기 위해서 앞에서 살펴본 바이트열로 부호화와 복호화라는 작업을 하게 된다.
여기서는 보편적인 방법에 대해서 살펴본다.
데이터베이스를 통한 데이터플로
데이터베이스에 기록하는 프로세스는 데이터를 부호화
데이터베이스에서 읽는 프로세스는 데이터를 복호화한다.
데이터베이스 내 호환성
- 하위 호환성 필요
예전 버전의 코드로 값을 기록, 신규 버전의 코드로 그 값을 읽을 수 있기 때문에 필요 - 상위 호환성 필요(순회식 업그레이드)
신규 버전의 코드로 값을 기록, 예전 버전의 코드로 그 값을 읽을 수 있기 때문에 필요 - 상위 호환성에서 추가적인 문제점
변환 과정에서 알지 못하는 필드가 유실될 수 있음
다양한 버전의 애플리케이션이 존재하는 경우 데이터 유실 예제
데이터 덤프
데이터 덤프는 한 번에 기록하고 이후에는 변하지 않으므로 아브로 객체 컨테이너 파일과 같은 형식이 적합
서비스를 통한 데이터플로: REST와 RPC
네트워크를 통해 통신해야 하는 프로세스가 있을 때, 가장 일반적으로 클라이언트와 서버 두 역할로 배치한다.
여기서 말하는 서비스 는 서버가 공개한 API이다.
예전 버전과 새로운 버전의 서버와 클라이언트가 동시에 실행될 수 있기에 서비스 API도 버전 간 호환이 가능해야 한다.
웹 서비스
웹 서비스는 서비스와 통신하기 위한 기본 프로토콜로 HTTP를 사용한다.
대중적인 두 가지 방법
- REST
- HTTP의 원칙을 토대로 한 설계 철학
- 간단한 데이터 타입을 강조
- URL을 사용해 리소스를 식별, 캐시 제어, 인증 등 HTTP 기능을 사용
- REST 원칙에 따라 설계된 API를 RESTful이라고 함
- SOAP
- XML 기반 프로토콜
- HTTP와 독립적이며 대부분의 HTTP 기능을 사용하지 않음
원격 프로시저 호출(RPC)
RPC 모델은 원격 네트워크 서비스 요청을 같은 프로세스 안에서 특정 프로그래밍 언어의 함수를 호출하는 것과 동일하게 사용 가능하게 해준다.
문제점
- RPC는 네트워크 요청, 따라서 로컬 함수 호출과는 매우 다름
- 로컬 함수 호출은 예측 가능하지만 네트워크 요청은 예측이 어려움
- 그래서 RPC는 여러 네트워크 문제에서 자유롭지 못 함
현재 방향
- 차세대 RPC 프레임워크는 원격 요청이 로컬 함수 호출과 다르다는 사실을 더욱 분명히 함
웹 서비스와 RPC 비교
REST의 장점
- 네트워크 프로토콜이라는 사실이 잘 들어남
- 실험과 디버깅에 적합
- 모든 주요 프로그래밍 언어와 플랫폼이 지원
- 사용 가능한 다양한 도구 생태계
RPC의 장점
- 이진 부호화 형식을 사용하는 사용자 정의 RPC 프로토콜의 우수한 성능 및 압축
서비스 내 호환성
- SOAP
요청과 응답은 XML 스키마로 지정되며 발전 가능하지만 일부 미묘한 함정이 존재 - RESTful API
응답에 JSON을 가장 일반적으로 사용
선택적 요청 매개변수 추가나 응답 객체의 새로운 필드 추가는 대개 호환성을 유지하는 변경으로 간주 - RPC
스키마의 상하위 호환 속성은 사용하는 부호화(스리프트, 프로토콜 버퍼, 아브로)로 부터 상속
호환성을 깨는 변경이 필요하면 서비스 제공자는 보통 여러 버전의 서비스 API를 함께 유지한다.
메시지 전달 데이터플로
여기서는 RPC와 데이터베이스 간 비동기 메시지 전달 시스템을 간단히 살펴본다.
메시지를 직접 네트워크로 전송하지 않고 메시지를 저장하는 메시지 브로커나 메시지 지향 미들웨어라는 중간 단계를 거쳐 전송한다.
RPC 대비 메시지 브로커의 장점
- 메시지 브로커가 버퍼처럼 동작하여 시스템 안정성 향상
- 메시지 유실을 방지
- 송신자가 수신자의 수신 정보를 알 필요가 없음
- 여러 수신자로 전송 가능
- 논리적으로 송신자와 수신자가 분리
수신자가 받은 메시지를 다시 게시한다면 데이터베이스에서 설명한 데이터 유실 문제가 발생할 수 있다.