[데이터중심 애플리케이션 설계] 4장 정리
애플리케이션은 필연적으로 시간이 지남에 따라 변해가는데, 기능을 변경하려면 대부분 저장하는 데이터도 변경해야 한다
새로운 필드를 추가해야 하거나, 기존 데이터를 새로운 방법으로 제공해야 할 수도 있다.
데이터 모델 별로 이런 변화에 대처하는 다양한 방법이 있다.
이런 방법들에 대해 알아보기 전에, 읽기/쓰기 스키마와, 상위/하위 호환성, 데이터 부호화에 대해 알아보자
읽기 스키마와 쓰기 스키마
애플리케이션이 파일이나 데이터베이스에 쓰기 위해 또는 네트워크 전송 등 목적으로 데이터를 스키마를 이용해 부호화 하는 것을 쓰기 스키마 라 한다.
반대로, 어떤 데이터를 복호화 하길 원한다면 이 스키마를 읽기 스키마라 한다.
관계형 데이터베이스는 모든 데이터가 하나의 스키마를 따른다고 가정하므로, 스키마는 변경될 수 있지만 특정 시점에는 정확히 하나의 스키마가 적용된다.
반면, 읽기 스키마 (스키마리스) 데이터베이스 (ex. nosql) 은 스키마를 강요하지 않으므로 다른 시점에 쓰여진 데이터 타입과 새로운 데이터 타입이 섞여 포함될 수 있다.
데이터 타입이나 스키마가 변경될 때 애플리케이션 코드의 변경이 있을수도 있다.
코드 변경은 대개 즉시 반영되지 못하므로, 이전 버전의 코드와 새로운 코드 (혹은 데이터 타입도) 공존할수 있으므로, 시스템이 원할히 동작하려면 호환성을 유지해야 한다
하위 호환성과 상위 호환성
하위호환성은, 새로운 코드는 예전 코드가 기록한 데이터를 읽을수 있어야 한다.
상위호환성은, 예전 코드는 새로운 코드가 기록한 데이터를 읽을수 있어야 한다.
새로운 코드는 이전 버전 코드가 기록한 데이터 형식을 알기에 해당 형식을 쉽게 다룰수 있지만, 예전 버전의 코드는 새 버전의 코드를 알지 못하기 때문에 상위 호환성은 지키기 좀 더 어렵다.
데이터 부호화 형식
프로그램은 보통 데이터를 두가지 형태로 표현한다.
메모리에 객체, 구조체, 리스트 등으로 데이터를 유지한다. CPU 에서 효율적으로 접근하고 조작할수 있게 최적화 한다.
데이터를 파일에 쓰거나 네트워크로 전송하려면 위와 같은 형태는 다른 프로세스가 이해할수 없으므로, 일련의 바이트열로 부호화 한다.
위 두가지 표현 사이의 전환이 필요하다.
인메모리 -> 바이트열로 전환을 부호화(직렬화 또는 마샬링)이라 하며, 반대를 복호화(파싱, 역직렬화, 언마샬링)이라 한다.
많은 프로그래밍 언어는 인메모리 객체를 부호화 하는 기능을 내장하여 편리하게 사용할 수 있지만, 여러 문제가 있어 일반적으로 좋지 않다.
이제 다양한 데이터 타입들의 데이터 부호화 방식과 호환성을 어떻게 지원하는지 살펴보자
JSON, XML
JSON 과 XML 표준화된 부호화라고 볼수 있을 정도로 많이 사용하고 있다.
JSON 과 XML 은 유니코드 문자열 (사람이 읽을수 있는)을 잘 지원하지만 이진 문자열(이진 부호화가 없는 바이트열)을 지원하지 않는다.
이진 문자열이 필요하다면 주로 Base64 인코딩을 통해 사용한다.
JSON 은 스키마를 지원하기는 하지만 익히고 구현하기 어려워 대부분 사용을 강제하지는 않는다.
JSON, XML, CSV 는 부호화와 스키마 사용에 결점이 있지만 다양한 용도로 사용되고 있다.
이진부호화
JSON 용으로 사용 가능한 다양한 이진 부호화 방식이 개발됐다.
이런 형식은 필요시에 사용되기는 하지만, JSON과 XML 처럼 많이 사용되지는 않는다.
JSON용 이진부호화 방식인 메시지팩을 살펴보자
앞으로의 예제에서 이진부호화에 사용할 레코드 예시이다.
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
부호화한 데이터는 필드개수, 필드별 길이, 필드내용 등을 표현한다.
이진부호화한 데이터 길이는 66바이트로, 일반 json부호화 81바이트보다 약간 작다.
JSON 의 모든 이진 부호화는 이와 비슷한데, 이정도의 공간 절약이 가독성을 해칠 만큼의 가치가 있는지는 불확실하다. 그래서 잘 안쓰이는듯..
쓰리프트, 프로토콜 버퍼
쓰리프트와 프로토콜버퍼는 같은 원리를 기반으로 한 이진 부호화 라이브러리다.
두 방식 모두 부호화할 데이터를 위한 스키마가 필요하다.
쓰리프트 스키마
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;
}
스리프트는 바이너리프로토콜, 컴팩트프로토콜 두가지 방식의 부호화 형식이 있다.
위 스키마와 바이너리 프로토콜을 이용해 부호화한 데이터는 다음과 같다
이 데이터는 필드 이름은 존재하지 않고 대신 필드 태그를 포함한다. 이 숫자는 스키마 정의에 있는 숫자이다.
컴팩트프로토콜을 이용한 부호화는 바이너리프로토콜과 비슷하지만 동일한 정보를 더 줄여 부호화 한다
프로토콜 버퍼는 이진 부호화 형식이 하나이고, 컴팩트 프로토콜과 매우 비슷하다.
스키마 발전
스리프트와 브로토콜 버퍼는 어떻게 호환성을 유지하며 스키마를 변경할까?
스키마 발전을 필드 태그 변경과 데이터 타입 변경 관점에서 살펴보자
필드 태그
부호화된 데이터를 해석하기 위해 필드 태그가 매우 중요하다. 필드 이름은 전혀 참조하지 않기 때문에 스키마에서 필드 이름은 변경할 수 있지만, 필드 태그는 변경할수 없다.
새로운 필드를 추가하고 싶다면 새로운 태그 번호를 부여 하면 된다.
예전 코드에서 새로운 코드로 기록한 데이터 (예전 코드가 인식할수 없는 태그 번호) 를 읽을때는 해당 필드를 무시할 수 있다.
이를 통해 상위 호환성을 유지할 수 있다.
초기 스키마 생성 이후 새로운 필드를 추가하려고 할때, 이 필드를 required 로 할 수 없다.
이렇게 추가할 경우, 이전의 데이터들은 추가한 필드에 대한 정보가 없으므로 새로운 코드가 이전의 데이터를 읽을때 이 작업은 실패한다.
따라서 하위 호환성을 유지하기 위해서는 추가되는 필드들은 optional 이거나 기본값을 가져야 한다.
데이터 타입 변경
데이터 타입 변경은 불가능 하지는 않지만 값이 부정확하거나 잘릴 위험이 있다.
예를 들어 32비트 정수를 64비트 정수로 바꾼다고 가정하면
새로운 코드는 이전의 데이터를 읽는것 (32비트 정수를 64비트 정수로 읽을 때) 은 가능하지만,
새로운 코드가 기록한 데이터를 이전 코드가 읽는 것 (64비트 정수를 32비트 정수로 읽을 때)는 값이 잘릴 수 있다.
아브로
아브로는 스피르트가 하둡 사용 사례에 적합치 않아 하둡 하위 프로젝트로 시작했다.
아브로도 부호화 데이터를 위해 스키마를 사용한다.
스키마는 사람이 편집할수 있는 아브로 IDL, 하나는 기계가 더 쉽게 읽을수 있는 JSON 기반 언어다.
record Person {
string userName;
union {null, long} favoriteNumber = null;
array<string> interests;
}
{
"type" : "record",
"name" : "Person",
"fields" : [
{"name":"userName", "type":"string"},
{"name":"favoriteNumber", "type":["null", "long"], "default" :null},
{"name":"interests", "type":{"type":"array", "items":"string"}}
]
}
아브로 스키마에는 태그 번호가 없다. 부호화된 바이트 데이터를 살펴보자
부호화된 데이터를 보면 필드나 데이터 타입을 식별하기 위한 정보가 없다.
이는 데이터를 읽는 코드와 기록한 코드가 같은 스키마를 사용한 경우에만 데이터를 제대로 복호화 할수 있음을 의미한다.
그렇다면 스키마 발전을 어떻게 제공할까?
핵심 아이디어는 쓰기 스키마와 읽기 스키마가 동일하지 않아도 되고 호환 가능하면 된다는 것이다.
데이터를 읽을때, 쓰기 스키마와 읽기 스키마를 함께 보고 쓰기 스키마에서 읽기 스키마로 데이터를 변환해 차이를 해소한다.
필드 순서가 다르거나 없더라도, 이름으로 필드를 일치시키기 때문에 문제 없다.
스키마의 장점
프로토콜 버퍼, 쓰리프트, 아브로는 스키마를 사용해 이진 부호화를 지원한다.
JSON, XML, CsV 같은 텍스트 타입도 많이 사용되지만 스키마 기반의 이진 부호화 또한 여러 장점을 갖고 있다.
- 텍스트 타입, 변형 이진 JSON 보다 크기가 훨씬 작을 수 있다.
- 스키마는 유용한 문서화 형식이다.
- 스키마 데이터베이스를 유지하면 상위 호환성과 하위 호환성을 확인할 수 있다
- 정적 타입 언어에서 스키마로부터 코드를 생성하는 기능은 유용하다.
데이터플로 모드
하나의 프로세스에서 다른 프로세스로 데이터를 전달하는 방법은 아주 많다.
데이터베이스를 통한 데이터플로
데이터베이스에 기록할때 데이터를 부호화 하고 읽을때 복호화 한다
일반적으로 동시에 여러 프로세스가 데이터베이스에 접근한다.
어떤 프로세스는 이전의 코드로 수행될 수도 있고, 어떤 프로세스는 다른 스키마로 접근할 수 있다.
이는 서로 다른 버전의 코드 (다른 스키마)로 데이터를 부호화/복호화 할수 있음을 의미한다.
따라서 주의하지 않는다면, 새로운 스키마로 부호화 한 데이터가 이전 스키마로 복호화 한후, 다시 부호화 하면 데이터가 유실될 가능성도 있다.
서비스를 통한 데이터플로 : REST, RPC
일반적인 방식으로 서버는 API 를 공개하고, 클라이언트는 이 API 를 사용한다.
서버 응답은 보통 사람이 편하게 볼수 있게 부호화한 (ex. JSON) 데이터다.
대용량 애플리케이션의 기능 영역을 소규모 서비스로 나누고, 해당 데이터가 필요하다면 해당 서비스에 요청을 보낸다.
이런 개발 방식을 전통적으로 서비스 지향 설계 (SOA) 라고 불렀으며 최근엔 이를 개선해 마이크로서비스 설계로 재탄생 했다.
위 설계의 핵심 목표는 서비스를 배포와 변경에 독립적으로 만들어 변경과 유지보수를 쉽게 하는것이다.
예전 버전과 새로운 버전의 서버와 클라이언트가 동시에 실행될 수 있고, API 버전간 호환이 가능해야 한다.
메시지 전달 데이터플로
비동기 메시지 전달 시스템으로, 메시지 브로커 (또는 메시지 큐) 를 통해 메시지를 전송한다
이는 여러 장점이 있다.
- 수신자가 사용 불가하거나 과부하 상태라면 브로커가 버퍼처럼 동작할수 있다
- 죽었던 프로세스에 메시지를 다시 전달할 수 있어 메시지 유실을 방지한다
- 송신자가 수신자가 누구인지 알 필요 없다
- 하나의 메시지를 여러 수신자에 전달할 수 있따
- 논리적으로 송신자와 수신자는 분리된다