티스토리 뷰
이번 주차는 4장 부호화와 발전에 대해 알아보았다.
애플리케이션은 필연적으로 시간에 따라 변한다.
첫 문장으로 시작한다.
입사한지 일주일정도 돼서 먼저 기존 소스코드를 파악하는 데 대부분의 시간을 보내고 있다.
이렇게 다시 소스코드나 ERD 구조를 하나하나 깊게 보는 것은 정말 오랜만이었다.
보면서 느낀게 위 첫문장과 일치하였다.
필드가 추가되기도 하고 어떤 테이블은 이제 사용하지 않기도 한다.
이 장에서는 이 변화에 대응하기 위해 어떻게 할 수 있을지 해결책을 알려주었다.
먼저 호환성 개념을 알아야 됐다.
하위 호환성: 새로운 코드는 예전 코드가 기록한 데이터를 읽을 수 있어야 한다.
상위 호환성: 예전 코드는 새로운 코드가 기록한 데이터를 읽을 수 있어야 한다. (이게 더 어렵다)
크게 1부와 2부로 나눌 수 있었다.
1부는 부호화 형식들에 대해 비교하였고 2부는 데이터 플로, 즉 프로세스간 데이터를 전달하는 어떤 방법이 있는지 소개하였다.
나는 회사 문서에서 ProtoBuf나 Avro에 대해 언급한 적이 있어서 이 쪽에 대해 좀 더 알아보았다.
1부: 부호화 형식 비교
메모리 속 객체를 바이트열로 바꾸는 방법들을 호환성 관점에서 바라보았다. (이를 부호화라고 한다.)
- 언어 내장 직렬화
언어 종속적인 문제, 보안 문제, 버전 관리의 부재
- 텍스트 형식
사람이 읽기 좋고 널리 쓰이지만, 숫자 정밀도 문제(json의 큰 정수), 이진 데이터 미지원, 스키마가 선택적이라는 약점
숫자 정밀도 문제
- XML과 CSV는 숫자 123과 문자열 "123"을 구분할 수 없다. (외부 스키마 없이는)
- JSON에서는 2^53보다 큰 정수부터는 정확히 표현이 안됨(JavaScript에서 파싱하는 순간 값이 미세하게 변함)


그래서

이진 데이터 미지원
- 텍스트 형식이라, 이미지나 암호화된 바이트 같은 이진 데이터는 담을 수 없음
Base64로 인코딩해서 문자열로 넣는 건 가능 (다만 데이터 크기 33% 증가)
스키마 선택적
- XML Schema, JSON Schema 존재하긴 하지만 실제로는 잘 안쓰는 곳이 많다고 함
- 이진 스키마 기반 형식
스키마를 명시적으로 정의하고, 필드 태그 번호(Thrift/Protocol Buffers)나 읽기•쓰기 스키마 분리(Avro)를 통해 스키마를 안전하게 발전 시킬 수 있다. (스키마 발전이란: 스키마는 필연적으로 시간이 지남에 따라 변한다)
여기서 각 프레임워크의 차이점과 언제 써야되는지 궁금했다.
1. Thirft / Protocol Buffers
스키마에 각 필드마다 태그 번호를 명시한다.
이 태그 번호는 절대 바꾸면 안된다. (이유: 부호화된 바이트열에는 필드 이름이 없고 태그 번호만 있어서, 태그 번호가 필드의 정체성)
# thrift
struct Person {
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}
# protocol buffers
message Person {
required string user_name = 1; // 정확히 1번
optional int64 favorite_number = 2; // 0번 또는 1번
repeated string interests = 3; // 0번 이상 몇 번이든
}
부호화된 바이트열에는 필드 이름이 아니라 태그 번호가 들어간다.
두 프레임워크 큰 차이는 없고 Protocol Buffers는 리스트 전용 타입이 없고 repeated표시자를 쓴다는 것 정도이다.
1. 필드 이름 변경: 자유
2. 새 필드 추가:
- 새 코드 -> (읽기) 옛 데이터 : 뒤를 돌아봄 = 하위 호환(required 금지)
- 옛 코드 -> (읽기) 새 데이터 : 앞을 내다봄 = 상위 호환(모르는 태그 건너뛰기로 자동)
3. 필드 삭제: optional 필드만 삭제 가능하고, 그 태그 번호는 영원히 재사용 금지
2. Avro (하둡 생태계에서 출발)
특이점: 태그 번호가 없음
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
부호화된 바이트열에는 태그, 필드 이름, 타입 정보 없음
단지 값들만 스키마에 정의된 순서대로 붙어 있음
유니온 타입이란: "이 필드는 이 타입들 중 하나일 수 있다."고 선언하는 방식.
union { null, long } favoriteNumber = null;
해석 -> favoriteNumber는 null이거나 long이다.
왜 필요한가?
Thirft나 Protobuf는 optional/required 표시자가 있지만, Avro는 없음
대신 더 엄격한 원칙이 있음 -> null도 하나의 타입이다.
- Protobuf/Thrift 에서는 어떤 필드든 그냥 비워둘 수 있음(optional이면)
- Avro에서는 필드 타입이 long이라고 선언했으면 무조건 long 이어야 함
- null을 포함하고 싶으면 유니온에 null을 명시적으로 포함시켜야 함
- Thirft/Protobuf optional: 필드가 바이트열에서 빠질 수 있음 -> 결과적으로 null 처럼 보임
- Avro union with null: 필드는 항상 있지만 그 값이 null일 수 있음
- null을 포함하고 싶으면 유니온에 null을 명시적으로 포함시켜야 함
그래서
유니온 타입은 optional을 표현하는 수단이자, 한 필드에 여러 타입을 허용하는 장치.
읽기 스키마와 쓰기 스키마
- 쓰기 스키마: 그 바이트열을 만들 당시 부호화에 사용된 스키마. 데이터와 함께 과거에 박제된 것이라, 지금 와서 바꿀 수 없습니다. "이 바이트열이 어떤 구조로 쓰였는가"의 답.
- 읽기 스키마: 지금 읽으려는 애플리케이션 코드가 기대하는 스키마. 코드에 컴파일되어 있는 것. "내 코드가 어떤 모양의 객체를 원하는가"의 답.
Avro는 바이트열에 아무것도 없이 값만 나열되어 있어서, 쓰기 스키마를 정확히 알아야 어디서 바이트들을 끊어 읽을지 알 수 있다.
그래서
1. 쓰기 스키마로 바이트열을 해석하고
2. 읽기 스키마로 비교해서 변환한다.
문제점
쓰기 스키마를 어떻게 전달하느냐는 문제가 생긴다.
해결책1
상황: 많은 레코드가 있는 대용량 파일일 때
모든 레코드가 같은 스키마인 경우여서 파일 맨 앞에 쓰기 스키마를 딱 한 번만 넣으면 된다.
Avro가 이를 위해 표준화한 파일 형식이 객체 컨테이너 파일이다.
해결책2
상황: 개별적으로 기록된 레코드를 가진 데이터베이스
각각 레코드들이 서로 다른 시점에 서로 다른 버전으로 쓰였을 때는 해결책1번이 유효하지 않음
이때는 아래처럼 해결

이런식으로 레지스트리에 스키마를 등록해서 컨슈머 쪽에서 조회해서 사용하도록 하면 된다.
동일한 스키마는 캐싱해서 사용할 수 있으니 I/O 비용은 첫 조회때만 발생한다.
해결책3
상황: 두 프로세스가 지속적인 양방향 연결로 통신하는 경우
연결을 맺는 시점에 "우리 이 스키마 버전으로 대화하자"고 합의, 연결이 살아있는 동안 그 스키마를 쓰면 된다.
Avro RPC가 이렇게 동작함
