본문 바로가기
TIL/데이터 엔지니어링

데이터 중심 어플리케이션 설계 - 4장. 부호화와 발전

by yeon_zoo 2023. 5. 22.

학습 목표 : 데이터 부호화를 위한 다양한 형식을 살펴보고, 스키마를 변경하고 예전 버전과 새로운 버전의 데이터와 코드가 공존하는 시스템 지원 방식을 알아본다. 그리고 REST, RPC, 액터(actor)와 같은 메세지 큐 전달 시스템에서 다양한 데이터 부호화 형식이 데이터 저장과 통신에 어떻게 사용되는지 살펴본다. 

 

양방향 호환성

  • 하위 호환성 : 새로운 코드는 예전 코드가 기록한 데이터(=예전 레코드)를 읽을 수 있어야 한다. 
  • 상위 호환성 : 예전 코드는 새로운 코드가 기록한 데이터(=새로운 레코드)를 읽을 수 있어야 한다. 

 

데이터 부호화 형식

프로그램은 최소 두 가지 형태로 표현된 데이터를 이용해서 동작한다. 데이터는 객체, 구조체, 리스트, 배열, 해시 테이블, 트리 등의 형태로 메모리에 유지되고 CPU에서 효율적으로 접근이나 조작이 가능하도록 최적화된다. 

 

데이터를 파일에 쓰거나 네트워크를 통해 전송할 때에는 메모리 내의 포인터 등을 사용할 수 없으니 일련의 바이트열 형태로 부호화가 필수적이다. 

*부호화 (=직렬화, 마샬링) : 인메모리 표현 -> 바이트열로의 전환

**복호화 (=파싱, 역직렬화, 언마샬링) : 바이트열 -> 읽어올 수 있도록 변환

 

언어별 형식

프로그래밍 언어마다 인메모리 객체에서 바이트열로 부호화하는 기능을 내장한다. (ex. java.io.Serializable) 내장 라이브러리를 사용하면 최소한의 추가 코드로 사용해서 편리하지만 다음과 같은 문제점이 있다. 

  1. 특정 언어랑 묶여서 다른 언어에서 읽기가 불가능하다. 
  2. 동일한 객체 유형의 데이터를 복원하려면 복호화 과정이 임의의 클래스를 인스턴스화 할 수 있어야 한다. -> 보안 문제의 원인이 될 수 있다. 
  3. 라이브러리는 버전 관리에 대해서 소홀할 수 있다. 쉬운 부호화를 위해서 상위, 하위 호환성 문제를 등한시하기 때문이다. 
  4. 효율성이 떨어진다. 성능은 낮고 용량만 비대해질 수 있다. 

JSON, XML, 이진 변형

JSON, XML, CSV : 텍스트 형식이라 사람이 쉽게 읽을 수 있다. 

단점

  1. 수 : XML, CSV - 수와 숫자로 된 문자열(3, "3") 구분이 없다. / JSON : 구분은 있지만 정수와 부동소수점을 구별하지 않아서 큰 수를 다룰 때 문제가 발생할 수 있다. 
  2. 유니코드 문자열 지원 / 이진 문자열(문자 부호화가 없는 바이트열) 지원하지 않음 : 따라서 이진 데이터를 base64로 부호화해서 사용해야 하는데 정공법이 아니기도 하고 데이터 크기도 증가한다. 
  3. JSON, XML : 스키마 지원 하지만 구현이 난해.
  4. CSV : 어플리케이션 내 코드로 각 로우, 컬럼을 정의

따라서 한 조직에서 다른 조직으로 전송하는 경우의 데이터 교환 형식에 적합하다. 

 

이진 부호화

조직 내 데이터라면 최소공통분모 부호화 형식(JSON, XML)의 필요성이 떨어지고 더 간편하고 파싱이 빠른 형태를 선택할 수 있다. 따라서 JSON/XML을 사용 가능한 다양한 이진 부호화의 개발을 진행한다. 이를 통해 JSON/XML의 데이터 모델은 유지하면서 데이터타입 셋을 확장시킬 수 있다.

가장 쉬운 방법은 스키마를 지정하지 않고 부호화된 데이터 안에 모든 객체 이름 필드를 저장하는 것이다. 다만 이 방법은 아주 약간의 공간 절약이 발생해서 큰 가치가 있다고 보기는 어렵다. 

 

스리프트와 프로토콜 버퍼 

공 : 스키마가 필요하고 필드마다 required / optional을 지정할 수 있다. 

스리프트 : 구성 = 데이터 타입, 필드 태그(어떤 필드인지를 지칭), 길이, 실제 데이터로 구성

  • 바이너리 프로토콜
  • 컴팩트 프로토콜 : 바이너리에서 필드 타입과 태그 숫자를 단일 바이트로 줄이고 가변 길이 정수로 부호화. 

프로토콜 버퍼 : 스리프트와 유사

장 : required를 사용 -> 이진 데이터 자체만으로는 필드의 필수 여부를 알 수 없지만 필드가 설정되지 않을 경우 실행 시 알 수 있어서 디버깅에 용이.

 

스리프트/프로토콜 버퍼의 필드 태그와 스키마 발전

  • 스키마 발전 : 시간이 지남에 따라 스키마가 변하는 것. 
  • 상위 호환성 : 필드 이름 변경 가능. 필드 태그 변경 불가능. 새로운 필드 태그 부여로 스키마에 필드 추가 가능. (이전 코드에서 레코드의 새로운 필드는 무시하면 된다)
  • 하위 호환성 : 새로운 코드가 예전 데이터를 항상 읽을 수 있다. 새로 추가된 필드는 required가 될 수 없기에 모든 추가 필드는 optional이거나 기본값 설정 필요. 

 

스리프트/프로토콜 버퍼의 데이터타입과 스키마 발전

  • 데이터타입 변경 시 값이 정확하지 않거나 잘릴 수 있다.
    ex) 32비트 정수 -> 64비트 정수
    • 상위 호환성 : 남은 32비트 0으로 채워서 읽을 수 있음. 보장 O
    • 하위 호환성 : 64비트인데 32비트까지만 읽음. 보장 X
  • 프로토콜 버퍼의 repeated 표시자 (리스트나 배열 대신 사용)
  • repeated 필드의 부호화 : 레코드에 단순히 동일한 필드 태그를 여러 번 작성하면 된다.
  • optional -> repeated로 변경될 시의 스키마 발전
    • 상위 호환성 : 보장 O. 필드가 존재하면 0이나 1개의 요소를 가진 리스트로 본다.
    • 하위 호환성 : 보장 O. 리스트의 마지막 요소만 보도록 한다. 

 

아브로

스키마에 정확히 일치하는 경우에만 이진 데이터를 올바르게 복호화할 수 있다. 스키마에 나타난 순서대로 필드를 살펴보고, 데이터 타입도 스키마를 통해 미리 파악한다. 읽기 스키마와 쓰기 스키마가 동일하고, 필드태그나 자료형이 데이터 안에서 부호화되고 있지 않기 때문에 지금까지의 부호화 중에서 길이가 가장 짧다. 

 

아브로의 쓰기 스키마와 읽기 스키마

  • 쓰기 스키마 : 알고 잇는 스키마 버전을 사용해 데이터 부호화
  • 읽기 스키마 : 데이터를 복호화하길 원할 때 특정 스키마로 복호화하길 기대한다. 
  • 아브로의 핵심 아이디어 : 쓰기 스키마와 읽기 스키마가 동일하지 않아도 되며 단지 호환 가능하면 된다. -> 데이터 복호화 시, 쓰기 스키마, 읽기 스키마를 함께 살펴보고 쓰기 스키마에서 읽기 스키마로 데이터를 변환해서 그 차이를 해소. ex) 필드 순서가 달라도 ok. 

 

아브로의 스키마 발전 규칙

  • 상위 호환성 : 새로운 버전의 쓰기 스키마와 예전 버전의 읽기 스키마를 가질 수 있음. 
  • 하위 호환성 : 새로운 버전의 읽기 스키마와 예전 버전의 쓰기 스키마를 가질 수 있음. 
  • 호환성 유지를 위해 기본값이 있는 필드만 추가하거나 삭제할 수 있다.
  • 필드에 널을 사용하려면 유니온 타입을 사용해야 함, 필드가 유니온 엘리먼트 중 첫 번째여야 기본값으로 널을 사용할 수 있다. > 널 가능 필드/ 널 불가능 필드를 명확히 구분하여 버그를 막는데 도움이 된다. 
  • 필드 이름 변경, 유니온 타입에 엘리먼트를 추가하는 것은 하위 호환성은 있지만 상위 호환성은 없다. 

 

그러면 쓰기 스키마는 무엇인가?

읽기는 특정 데이터를 부호화한 쓰기 스키마를 어떻게 알 수 있을까? 아브로를 사용하는 상황에 따라 다르다. 

  • 많은 레코드가 있는 대용량 파일 : 파일의 쓰기는 파일의 시작 부분에 한 번만 쓰기 스키마를 포함시킨다. 이를 위해 파일형식(객체 컨테이너 파일)을 명시한다. 
  • 개별적으로 기록된 레코드를 가진 DB : 서로 다른 시점에 기록되고 동일한 스키마가 아닐 수 있다. 따라서 모든 부호화된 레코드의 시작 부분에 버전 번호 포함 + DB에는 스키마 버전 목록 유지
  • 네트워크 연결을 통해 레코드 보내기 : 양방향 네트워크 연결로 통신할 때 연결 설정에서 스키마 버전 합의하고 연결이 유지되는 동안 합의된 스키마를 사용한다. 아브로 RPC 프로토콜이 이처럼 동작

 

동적 생성 스키마

아브로의 장점 : 스키마에 태그 번호가 포함되어 있지 않다. > 아브로가 동적 생성 스키마에 더 친숙함. 

관계형 DB의 데이터를 덤프한다고 가정하면)

  • 관계형 스키마 > 아브로 스키마를 만들고 DB 내용을 부호화해서 덤프한다. 
  • 스키마 변경이 있으면 아브로 스키마를 갱신하고 다시 덤프한다. 
  • 상위 호환성 : 필드는 이름으로 식별되기 때문에 이전 읽기 스키마로 읽을 수 있다. 
  • 스리프트, 프로토콜 버퍼는 이 경우에 수동으로 컬럼 이름과 필드 태그의 매핑을 해결해야 한다. 

 

코드 생성과 동적 타입 언어

  스리프트, 프로토콜 버퍼 아브로
코드 생성 코드 생성에 의존 -> 정적 타입 언어에서 유리
효율적인 인메모리 구조를 사용, IDE에서 타입 확인과 자동 완성 가능
코드 생성 중요하지 않음.
동적 생성 스키마에서 데이터를 가져오는 데 불필요함.
코드 생성 선택적 제공 for 정적 타입 프로그래밍 언어

아브로가 코드 생성을 제공하긴 하지만 코드 생성 없이도 사용할 수 있다. 자기 기술(self-describing)적인 객체 컨테이너 파일(쓰기 스키마 포함됨)이 있다면 아브로 라이브러리를 사용해서 간단히 열어 데이터를 볼 수 있다. 

 

스키마의 장점

간단하고 더 자세한 유효성 검사 규칙을 지원한다. 

많은 데이터 시스템이 이진 부호화를 독자적으로 구현한다. (+ 복호화 드라이버 ex. ODBC, JDBC API)

이진부호화의 장점

  • 부호화된 데이터에서 필드 이름을 생략할 수 있어서 크기를 작게 유지할 수 있다. 
  • 복호화 시 스키마가 최신 상태인지를 확신할 수 있다. 
  • 스키마 DB 유지 시 스키마 변경 적용 전에 상위 호환성과 하위 호환성을 확인할 수 있다. 
  • 정적 타입 프로그래밍 언어에선 컴파일 시점에 타입 체크를 할 수 있기 때문에 유용하다. 

: 스키마 발전은 스키마리스 또는 읽기 스키마 JSON DB가 제공하는 것과 동일한 종류의 유연성을 제공하며 데이터나 도구 지원도 더 잘 보장한다. 

 

데이터플로 모드 

데이터 플로 : 하나의 프로세스에서 다른 프로세스로 데이터를 전달하는 방법

  • 데이터베이스를 통해
  • 서비스 호출을 통해
  • 비동기 메시지 전달을 통해

DB를 통한 데이터플로

DB에 저장하는 것 = 미래의 자신에게 메시지를 보내는 일. > 이전에 기록한 내용을 미래의 자신이 보기 위한 하위 호환성 필요. + 여러 프로세스가 동시에 접근하는 일이 잦으므로 (ex. 순회식 업그레이드) 상위 호환성도 필요.

 

다양한 시점에 기록된 다양한 값

데이터가 코드보다 더 오래 산다. 

마이그레이션은 높은 비용. > 기존 데이터를 다시 기록하지 않고 널을 기본값으로 갖는 새로운 칼럼을 추가하는 방식으로 사용

여러 가지 버전의 스키마로 부호화된 레코드를 포함해도 전체 DB가 단일 스키마로 부호화된 것처럼 보이게 한다. 

 

보관 저장소

데이터 덤프는 아브로 객체 컨테이너 파일과 같은 형식이 적합하다. (한 번에 기록하고 이후에는 변하지 않으므로)

 

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

서버간 데이터 부호화는 API 버전 간 호환이 필요하다.

  • REST : HTTP 원칙을 토대로 한 설계 철학
  • SOAP : 네트워크 API 요청을 위한 XML 기반 프로토콜 (HTTP 기능 사용하지 않음)

원격 프로시저 호출(RPC) 문제

원격 네트워크 서비스 요청을 같은 프로세스 안에서 특정 프로그래밍 언어의 함수나 메서드를 호출하는 것과 동일하게 사용 가능하게 해준다(= 위치 투명성).  네트워크 요청은 로컬 함수 호출과는 다르다는 점에서 그 결함이 드러난다. (로컬 함수 호출 : 예측 가능 / 네트워크 문제 : 예측이 어려움)

  • 타임아웃 
  • 응답만 유실 (중복 제거 기법=멱등성을 적용하지 않으면 재시도는 작업이 여러 번 수행되는 원인이 된다)
  • 네트워크 요청은 함수 호출보다 훨씬 느리고 지연시간이 매우 다양
  • 로컬 함수는 포인터를 효율적으로 전달할 수 있으나 네트워크는 바이트열 부호화 필요
  • 언어가 다르면 데이터타입이 다를 수 있고, 깔끔하지 않아짐. 

 

RPC의 현재 방향

같은 데이터센터 내의 같은 조직이 소유한 서비스 간 요청에 집중

 

데이터 부호화와 RPC의 발전

RPC 클라이언트와 서버를 독립적으로 변경하고 배포할 수 있어야 한다. 모든 서버를 먼저 갱신하고 나서 모든 클라이언트를 갱신해도 문제가 없다고 가정한다. => 요청은 하위 호환성만 필요, 응답은 상위 호환성만 필요

but 조직 경계를 넘나드는 통신에 사용된다면 호환성을 거의 무한정 유지해야 하고, 호환성을 깨야 한다면 여러 버전의 서비스 API를 함께 유지해야 한다. 

 

메시지 전달 데이터플로

  • REST, RPC : 하나의 프로세스가 네트워크를 통해 다른 프로세스로 요청을 전송하고 가능한 빠른 응답을 기대하는 방식.
  • 데이터베이스 : 하나의 프로세스가 부호화한 데이터를 기록하고 다른 프로세스가 언젠가 그 데이터를 다시 읽는 방식.
  • 비동기 메시지 전달 시스템 : 클라이언트 요청을 낮은 지연 시간으로 다른 프로세스에 전달하고, 메시지 브로커(메시지 큐), 메시지 지향 미들웨어라는 중간 단계를 둔다. 

 

메시지 브로커 vs. RPC

  • 수신자가 사용 불가능한 상태이면 메시지 브로커가 버퍼처럼 동작 가능. > 시스템 안정성 향상
  • 죽었던 프로세스에 메시지를 다시 전달할 수 있기 때문에 메시지 유실을 방지
  • 송신자가 수신자의 IP 주소나 포트 번호를 알 필요가 없음.
  • 하나의 메시지를 여러 수신자로 전송
  • pub-sub 패턴으로 둘을 분리
  • 메시지 전달 통신은 단방향이다(응답을 기대하지 않음) + 비동기 

 

메시지 브로커

프로세스 하나가 메시지를 이름이 지정된 큐나 토픽으로 전송하고 브로커는 해당 큐나 토픽 하나 이상의 소비자/구독자에게 메시지를 전달한다. 동일 토픽에 여러 생산자와 소비자가 있을 수 있다. 

부호화가 상하위 호환성을 모두 가진다면 pub, sub을 독립적으로 변경해 임의 순서로 배포할 수 있는 유연성을 가진다. 

소비자가 다른 토픽으로 메시지를 다시 게시한다면 새 코드로 기록한 데이터를 옛 코드가 다시 썼을 때 데이터가 유실되는 문제를 방지할 목적으로 알지 못하는 필드 보존에 주의가 필요하다. 

 

분산 액터 프레임워크

액터 모델 : 단일 프로세스 안에서 동시성을 위한 프로그래밍 모델. 스레드를 직접 처리하는 대신 로직이 액터에 캡슐화된다. 

  • 각 액터 = 하나의 클라이언트나 엔티티.
  • 액터는 로컬 상태를 가질 수 있고 비동기 메시지의 송수신으로 다른 액터와 통신
  • 액터는 메시지 전달을 보장하지 않으므로 유실될 수 있음. 
  • 각 액터 프로세스는 한 번에 하나의 메시지만 처리하기 떄문에 프레임워크와 독립적으로 실행 가능.

 

댓글