개발독서/데이터

[고투런1기] 데이터 중심 애플리케이션 설계 (4장 부호화와 발전)

보리시스템 2024. 5. 24.
[1부 데이터 시스템의 기초]
4장 부호화와 발전

1. 데이터 부호화 형식

1.1 언어별 형식
1.2 JSON과 XML, 이진 변형
1.3 스리프트와 프로토콜 버퍼
1.4 아브로
1.5 스키마의 장점

2. 데이터플로 모드

2.1 데이터베이스를 통한 데이터플로
2.2 서비스를 통한 데이터플로: REST와 RPC
2.3 메시지 전달 데이터플로

 

[정리: 데이터 중심 애플리케이션 설계를 뒷받침하는 근본 개념을 설명]

- 데이터 구조를 네트워크나 디스크 상의 바이트열로 변환하는 다양한 방법
=> 부호화의 세부 사항은 효율성, 애플리케이션의 아키텍처, 배포의 선택 사항에도 영향

- 많은 서비스가 새로운 버전의 서비스를 동시에 모든 노드에 배포하는 방식보다 한 번에 일부 노드에만 서서히 배포하는 순회식 업그레이드가 필요
=> 순회식 업그레이드? 정지 시간 없이 새로운 버전의 서비스를 출시 가능하게 하고, 배포를 덜 위험하게 함

- 시스템을 흐르는 모든 데이터는 상/하위 호환성 제공하는 방식으로 부호화해야 함
=> 상위 호환성? 예전 코드가 새로운 데이터를 읽을 수 있음
=> 하위 호환성? 새로운 코드가 예전 데이터를 읽을 수 있음

- 데이터 부호화 형식과 호환성 속성
1) 프로그래밍 언어에 특화된 부호화는 단일 프로그래밍 언어로 제한되며, 상위/하위 호환성을 제공하지 못하는 경우가 종종 있음
2) JSON, XML, CSV 같은 텍스트 형식은 널리 사용됨
=> 이들간 호환성은 데이터타입을 사용하는 방법에 달려 있어 스키마가 있으면 유용할 수 있으나 반대로 방해가 됨
3) 스리프트, 프로토콜 버퍼, 아브로 같은 이진 스키마 기반 형식은 짧은 길이로 부호화되며 명확하게 정의된 상/하위 호환성의 맥락에서 효율적인 부호화를 지원함
=> 정적 타입 언어에서 문서와 코드 생성에 유용하지만 사람이 읽기 위해서는 복호화 필요

- 데이터 부호화의 중요성에 대한 여러 시나리오를 보여주는 데이터플로 모드
1) DB에 기록하는 프로세스가 부호화하고 DB에 읽는 프로세스가 복호화하는 DB
2) 클라이언트가 요청을 부호화하고 서버는 요청을 복호화하고 응답을 부호화하고 최종적으로 응답을 복호화하는 RPC와 REST API
3) 송신자가 부호화하고 수신자가 복호화하는 메시지를 서로 전송해서 노드간 통신하는 비동기 메시지전달 (메시지브로커나 액터를 이용)

 


4장 부호화와 발전

- 시스템이 원활히 실행되게 하려면 양방향으로 호환성 유지해야 함
1) 하위 호환성: 새로운 코드는 예전 코드가 기록한 데이터를 읽을 수 있어야 함
=> 다루기 어렵지 않음

2) 상위 호환성: 예전 코드는 새로운 코드가 기록한 데이터를 읽을 수 있어야 함
=> 예전 버전의 코드가 새 버전의 코드에 의해 추가된 것을 무시할 수 있어야 하므로 다루기 더 어려움

- JSON, XML, 프로토콜 버퍼, 스리프트(Thrift), 아브로(Avro)를 비롯한 데이터 부호화를 위한 다양한 형식을 살펴봄
=> 어떻게 스키마를 변경하고 예전 버전과 새로운 버전의 데이터와 코드가 공존하는 시스템을 어떻게 지원하는지 설명
=> 웹서비스에서의 대표 상태 전달(REST), 원격 프로시저 호출, 액터/메시지큐 같은 메시지 전달 시스템에서의 데이터 부호화 형식, 데이터 저장 통신에 대해 설명

 


 

1. 데이터 부호화 형식

- 프로그램은 보통 2가지 형태로 표현된 데이터를 사용해 동작함
=> 메모리에 객체, 구조체, 목록, 배열, 해시 테이블, 트리 등으로 데이터가 유지됨 (CPU에서 효율적으로 접근하고 조작할 수 있게 보통 포인터를 이용해 최적화됨)
=> 데이터를 파일에 쓰거나 네트워크를 통해 전송하려면 스스로를 포함한 일련의 바이트열의 형태로 부호화해야 함 (포인터는 다른 프로세스가 이해할 수 없으므로 일련의 바이트열은 보통 메모리에서 사용하는 데이터 구조와 다름)

 


 

1.1 언어별 형식 

- 많은 프로그래밍 언어는 인메모리 객체를 바이트열로 부호화하는 기능을 내장함
=> 자바 java.io.Serializable, 루비 Marshal, 파이썬 pickle
=> 매우 편리하지만 문제점도 있으므로 일시적인 목적 외에 사용하는 것은 좋지 않음

- 문제점?
1) 부호화는 보통 특정 프로그래밍 언어와 묶여 있어 다른 언어에서 데이터를 읽기는 매우 어려움 
2) 동일한 객체 유형의 데이터를 복원하려면 복호화 과정이 임의의 클래스를 인스턴스화할 수 있어야 함
3) 데이터 버전 관리는 보통 부호화 라이브러리에서는 나중에 생각하게 됨
4) 효율성도 나중에 생각하게 됨

 


 

1.2 JSON과 XML, 이진 변형

- JSON, XML, CSV의 문제점?
1) 수의 부호화에는 애매함이 있고, 큰 수를 다룰 때 문제가 됨
2) 유니코드 문자열은 지원하지만 이진 문자열은 지원하지 않음
3) XML, JSON 모두 스키마를 지원하지만 익히고 구현하기 난해함
4) CSV는 스키마가 없어 각 로우, 칼럼의 의미를 정의하는 작업을 애플리케이션이 해야 함

- JSON, XML, CSV는 결점이 있지만 데이터 교환 형식으로 사용하기에 매우 좋으므로 앞으로도 사용될 것임

 

  • 이진 부호화
- JSON은 XML보다 덜 장황하지만 이진 형식에 비해 많은 공간을 이용함
=> 틈새 시장에서는 채택됐지만 JSON, XML 텍스트 버전처럼 널리 채택되지는 않음

 


 

1.3 스리프트와 프로토콜 버퍼

- 스리프트(페이스북 개발), 프로토콜 버퍼(구글 개발)는 같은 원리를 기반으로 한 이진 부호화 라이브러리임
=> 부호화할 데이터를 위한 스키마가 필요함

- 스리프트는 2가지 다른 이진 부호화 형식이 있음
1) 바이너리 프로토콜(Binary Protocol)
2) 컴팩트 프로토콜(Compact Protocol)
=> 의미상으로는 같지만 컴팩트 프로토콜이 바이너리 프로토콜보다 동일한 정보를 더 줄여서 부호화함

 

  • 필드 태그와 스키마 발전
- 스키마 발전(schema evolution)? 스키마는 필연적으로 시간이 지남에 따라 변함

- 스리프트, 프로토콜 버퍼는 하위/상위 호환성을 유지하며 어떻게 스키마를 변경할까?
1) 상위 호환성 유지 방법
=> 필드에 새로운 태그 번호를 부여하는 방식으로 스키마에 새로운 필드를 추가할 수 있음
=> 예전 코드에서 새로운 코드로 기록한 데이터를 읽을 경우 해당 필드를 간단히 무시할 수 있음
=> 데이터타입 주석은 파서가 몇 바이트를 건너뛸 수 있는지 알려줌(이는 상위 호환성을 유지하게 함)

2) 하위 호환성 유지 방법
=> 각 필드에 고유한 태그 번호가 있는 동안에는 태그 번호가 계속 같은 의미를 가지고 있기 때문에 새로운 코드가 예전 데이터를 항상 읽을 수 있음
=> 하위 호환성을 유지하려면 스키마의 초기 배포 후에 추가되는 모든 필드는 optional로 하거나 기본값을 가져야 함

 

  • 데이터타입과 스키마 발전
- 필드 데이터타입을 변경하는 건 값이 정확하지 않거나 잘릴 위험이 있음

- 스리프트에는 전용 목록 데이터타입이 있는데 목록 엘리먼트의 데이터타입을 매개변수로 받음
=> 목록 데이터타입은 프로토콜 버퍼와는 다르게 단일 값에서 다중 값으로의 변경을 허용하지 않지만 중첩된 목록을 지원한다는 장점이 있음

 


 

1.4 아브로

- 부호화할 데이터 구조를 지정하기 위해 스키마를 사용함
=> 아브로를 이용해 이진 데이터를 파싱하려면 스키마에 나타난 순서대로 필드를 살펴보고 스키마를 이용해 각 필드의 데이터타입을 미리 파악해야 함
=> 데이터를 읽는 코드가 데이터를 기록한 코드와 정확히 같은 스키마를 사용하는 경우에만 이진 데이터를 올바르게 복호화할 수 있음
=> 읽기, 쓰기 간 스키마가 불일치한다면 데이터 복호화가 정확하지 않음

 

  • 쓰기 스키마와 읽기 스키마
- 쓰기 스키마? writer's schema
=> 애플리케이션이 파일이나 DB에 쓰기 위해 또는 네트워크를 통해 전송 등의 목적으로 어떤 데이터를 아브로로 부호화하길 원한다면 알고 있는 스키마 버전을 사용해 데이터를 부호화함. 애플리케이션에 포함될 수 있는 이러한 스키마를 쓰기 스키마라고 함

- 읽기 스키마? reader's schema
=> 애플리케이션이 파일이나 DB에서 또는 네트워크로부터 수신 등으로 읽은 어떤 데이터를 복호화하길 원한다면 데이터가 특정 스키마로 복호화하길 기대하는데 이 스키마를 읽기 스키마라고 함
=> 애플리케이션 코드는 읽기 스키마에 의존함
=> 복호화 코드는 애플리케이션을 빌드하는 동안 스키마로부터 생성됨

- 아브로의 핵심 아이디어는 쓰기/읽기 스키마가 동일하지 않아도 되며 단지 호환 가능하면 된다는 것임

 

  • 쓰기 스키마는 무엇인가?
- 읽기는 특정 데이터를 부호화한 쓰기 스키마를 어떻게 알 수 있을까?
=> 모든 레코드에 전체 스키마를 포함시킬 수 없음 (스키마는 부호화된 데이터보다 훨씬 클 가능성이 있기 때문임)

- 아브로를 사용하는 상황에 따라 다름
1) 많은 레코드가 있는 대용량 파일
=> 파일의 쓰기는 파일의 시작 부분에 한 번만 쓰기 스키마를 포함시키면 됨 (이브로는 이를 위해 파일 형식을 명시함)

2) 개별적으로 기록된 레코드를 가진 DB
=> 모든 부호화된 레코드의 시작 부분에 버전 번호를 포함하고 DB에는 스키마 버전 목록을 유지

3) 네트워크 연결을 통해 레코드 보내기
=> 두 프로세스가 양방향 네트워크 연결을 통해 통신할 때 연결 설정에서 스키마 버전 합의를 할 수 있음. 이후 연결을 유지하는 동안 합의된 스키마를 사용함

 

  • 동적 생성 스키마
- 아브로의 장점으로 스키마에 태그 번호가 포함돼 있지 않다는 것이 있음
=> 아브로는 동적 생성 스키마에 더 친숙함

- 이와 달리 스리프트, 프로토콜 버퍼는 DB 스키마가 변경될 때마다 관리자는 DB 칼럼 이름과 필드 태그의 매핑을 수동으로 갱신해야 함

 

  • 코드 생성과 동적 타입 언어
- 스리프트, 프로토콜 버퍼는 코드 생성에 의존함
=> 스키마를 정의한 후 선택한 프로그래밍 언어로 스키마를 구현한 코드를 생성할 수 있음
=> 정적 타입 언어에서 유용함
=> 동적 타입 언어에서는 만족시킬 컴파일 시점의 타입 검사기가 없기 때문에 코드 생성은 중욯자ㅣ 않음

- 아브로는 정적 타입 언어를 위해 코드 생성을 선택적으로 제공함
=> 하지만 코드 생성 없이도 사용할 수 있음
=> 아파치 피그 같은 동적 타입 데이터 처리 언어와 함께 사용할 때 특히 유용함

 


 

1.5 스키마의 장점

- 스키마리스, 읽기 스키마 JSON DB가 제공하는 것과 동일한 종류의 유연성을 제공함
- 데이터나 도구 지원도 더 잘 보장함

 


 

2. 데이터플로 모드

- 데이터플로는 매우 추상적인 개념으로 하나의 프로세스에서 다른 프로세스로 데이터를 전달하는 방법은 많음

 


 

2.1 데이터베이스를 통한 데이터플로

- DB에 기록하는 프로세스는 데이터를 부호화하고 DB에서 읽는 프로세스는 데이터를 복호화함
=> 하위 호환성이 필요한데 그렇지 않으면 이전에 기록한 내용을 미래의 자신이 복호화할 수 없음

 

  • 다양한 시점에 기록된 다양한 값
- 데이터가 코드보다 더 오래 산다(data outlives code)

- 데이터를 새로운 스키마로 다시 기록하는 마이그레이션 작업은 가능하지만 대용량 데이터셋 대상으로는 비싼 작업이기 때문에 대부분의 DB에서 이러한 상황은 피함

- 스키마 발전은 기본 저장소가 여러 버전의 스키마로 부호화된 레코드를 포함해도 전체 DB가 단일 스키마로 부호화된 것처럼 보이게 함

 

  • 보관 저장소
- 데이터의 복사본은 일관되게 부호화하는 것이 좋음
=> 데이터 덤프는 한 번에 기록하고 이후에는 변하지 않으므로 아브로 객체 컨테이너 파일과 같은 형식이 적합함

 


 

2.2 서비스를 통한 데이터플로: REST와 RPC

- 서버가 공개한 API를 서비스라고 함

- 서비스는 DB와 유사함
=> 서비스는 클라이언트가 데이터를 제출하고 질의하는 것을 허용함
=> 하지만 DB는 질의 언어를 이용한 임의 질의를 허용하지만 서비스는 서비스의 비즈니스 로직(애플리케이션 코드)으로 미리 정해진 입력과 출력만 허용한 애플리케이션 특화 API를 공개함

- 서버와 클라이언트가 사용하는 데이터 부호화는 서비스 API의 버전 간 호환이 가능해야 함 

 

  • 웹 서비스
- 웹 서비스? 서비스와 통신하기 위한 기본 프로토콜로 HTTP를 사용할 때 이를 웹서비스라고 함

- 웹 서비스에는 대중적인 2가지 방법이 있음
1) REST
=> 프로토콜이 아닌 HTTP 원칙을 토대로 한 설계 철학
=> 간단한 데이터 타입을 강조
=> URL을 사용해 리소스를 식별하고 캐시 제어, 인증, 콘텐츠 유형 협상에 HTTP 기능을 사용함

2) SOAP
=> 웹 서비스 기술 언어(WSDL)이라고 부르는 XML 기반 언어를 사용해 기술함
=> 클라이언트가 로컬 클래스와 메서드 호출을 사용해 원격 서비스에 접근하는 코드 생성이 가능함
=> SOAP 메시지를 수동으로 구성하기는 너무 복잡하기 때문에 도구 지원, 코드 생성, IDE에 크게 의존함

 

  • 원격 프로시저 호출(RPC) 문제
- 웹 서비스는 1970년대부터 사용한 원격 프로시저 호출의 아이디어를 기반으로 함

- RPC 모델은 원격 네트워크 서비스 요청을 같은 프로세스 안에서 특정 프로그래밍 언어의 함수나 메서드를 호출하는 것과 동일하게 사용 가능하게 함

- 네트워크 요청은 예측이 어려움
=> 네트워크 문제로 요청과 응답이 유실되거나 원격 장비가 느려지거나 요청에 응답하지 않을 수 있음

 

  • RPC의 현재 방향
- 이런 문제에도 RPC는 사라지지 않음

- 차세대 RPC 프레임워크는 원격 요청이 로컬 함수 호출과 다르다는 사실을 분명히 함
=> 서비스 찾기를 제공하기도 함(클라이언트가 특정 서비스를 찾을 수 있는 IP 주소와 포트 번호를 제공)

- RPC 프레임워크의 주요 초점은 보통 같은 데이터센터 내의 같은 조직이 소유한 서비스 간 요청에 있음

 

  • 데이터 부호화와 RPC의 발전
- 발전성이 있으려면 RPC 클라이언트와 서버를 독립적으로 변경하고 배포할 수 있어야 함

- RPC가 조직 경계를 넘나드는 통신에 사용되기 때문에 서비스 호환성 유지를 더욱 어렵게 함

- RESTful API는 URL이나 HTTP Accept 헤더에 버전 번호를 사용하는 방식이 일반적임
=> 특정 클라이언트를 식별하는 데 API 키를 사용하는 서비스는 클라이언트의 요청 API 버전을 서버에 저장한 뒤 버전 선택을 별도 관리 인터페이스를 통해 갱신할 수 있게 하는 것이 한가지 방법임

 


 

2.3 메시지 전달 데이터플로

- RPC와 DB 간 비동기 메시지 전달 시스템

- 메시지 브로커를 사용하는 방식은 직접 RPC를 사용하는 방식과 비교했을 때 여러 장점이 있음
1) 수신자가 가용 불가능하거나 과부화 상태라면 메시지 브로커가 버퍼처럼 동작할 수 있기 때문에 시스템 안정성이 향상됨
2) 죽었던 프로세스에 메시지를 다시 전달할 수 있기 때문에 메시지 유실을 방지함
3) 송신자가 수신자의 IP 주소나 포트 번호를 알 필요가 없음
4) 하나의 메시지를 여러 수신자로 전송할 수 잇음
5) 논리적으로 송신자는 수신자와 부리됨 

 

  • 메시지 브로커
- 프로세스 하나가 메시지를 이름이 지정된 큐나 토픽으로 전송하고 브로커는 해당 큐나 토픽 하나 이상의 소비자 또는 구독자에게 메시지를 전달함
=> 동일한 토픽에 여러 생산자와 소비자가 있을 수 있음
=> 토필은 단반향 데이터플로만 제공함
=> 하지만 소비자 스스로 메시지를 다른 토픽으로 게시하거나 원본 메시지의 송신자가 소비하는 응답 큐로 게시할 수 있음

- 메시지 브로커는 보통 특정 데이터 모델을 가용하지 않음
=> 메시지는 일부 메타데이터를 가진 바이트열이므로 모든 부호화 형식을 사용할 수 잇음

 

  • 분산 액터 프레임워크
- 앱터 모델은 단일 프로세스 안에서 동시성을 위한 프로그래밍 모델임
=> 스레드를 직접 처리하는 대신 로직이 액터에 캡슐화됨
=> 보통 각 액터는 하나의 클라이언트나 엔티티를 나타냄

- 분산 액터 프레임워크에서 이 프로그래밍 모델은 여러 노드 간의 애플리케이션 확장에 사용 됨
=> 송신자와 수신자가 같은 노드에 있는지 다른 노드에 있는지 관계없이 동일한 메시지 전달 구조를 사용함

- 액터 모델은 단일 프로세스 안에서도 메시지가 유실될 수 있다고 이미 가정하므로 위치 투명성은 RPC보다 액터 모델에서 더 잘 동작함

- 분산 액터 프레임워크의 메시지 부호화 처리 방법
1) 아카(Akka)
=> 자바의 내장 직렬화를 사용

2) 올리언스(Orleans)
=> 사용자 정의 데이터 부호화 형식을 사용

3) 얼랭(erlang)
=> OTP에서는 레코드 스키마를 변경하는 일이 의외로 어려움. 순회식 업그레이드는 가능하지만 신중하게 계획해야 함