[데이터 중심 애플리케이션 설계] 04. 부호화와 발전

예니·2023년 2월 4일
0
post-thumbnail

애플리케이션은 항상 변한다. 데이터도 변경해야 한다. 변화할 때 시스템이 원활히 실행되게 하려면 양방향으로 호환성을 유지해야 한다.

데이터 부호화 형식

프로그램은 보통 두가지 형태로 표현되 데이터를 사용한다.

  • 메모리에 객체, 구조체, 목록, 배열 등으로 데이터가 유지된다. 이런 데이터 구조는 CPU에서 효율적으로 접근하고 조작할 수 있다.
  • 데이터를 파일에 쓰거나 네트워크를 통해 전송하려면 스스로를 포함한 일련의 바이트열의 형태로 부호화해야 한다. 메모리에서 사용하는 데이터 구조와 다르다.
  • 부호화, 복호화
    • 부호화 (직렬화, 마샬링) : 인메모리 → 바이트열
    • 복호화 (파싱, 역직렬화, 언마샬링) : 바이트열 → 인메모리

언어별 형식

프로그래밍 언어 자체에 인메모리 객체를 바이트열로 부호화하는 기능을 내장하지만, 다음과 같은 문제점들이 있어 사용하지 않는 편이 좋다.

  • 특정 언어에 종속되어 다른 언어에서 데이터 읽기 어렵다.
  • 동일한 객체 유형의 데이터를 복원하려면 복호화 과정이 임의의 클래스를 인스턴스화할 수 있어야하고, 이는 보안 이슈가 있다.
  • 데이터 버전 관리, 효율성을 고려하지 않게 된다.

JSON, XML, 이진 변형

JSON, XML, CSV는 텍스트 형식이라 사람이 읽을 수 있고, 편리하여 널리 쓰이지만 다음과 같은 문제가 있다.

  • 수(number) 부호화가 애매하다. XML, CSV는 수와 숫자로 표현된 문자열을 구분하지 못한다. JSON은 이 둘은 구분하지만 정수와 부동소수점 수를 구별하지 않는다.
  • 이는 큰 수를 다룰 때 문제가 된다.
  • JSON, XML은 유니코드 문자열을 지원하지만, 이진 문자열을 지원하지 않는다.
  • XML, JSON 모두 스키마를 지원하지만, 구현하기 난해하다.
  • CSV는 스키마가 없어, 의미 정의를 애플리케이션이 해야 한다.

이진 부호화

JSON은 XML보다 덜 장황하지만 이진 형식에 비하면 둘 다 많은 공간을 차지한다.

스리프트와 프로토콜 버퍼

  • 둘은 같은 원리를 기반으로 한 이진 부호화 라이브러리이다. 스리프트(페이스북), 프로토콜 버퍼(구글)
  • 둘 다 부호화할 데이터를 위한 스키마가 필요하다. 스키마 정의를 사용해 코드를 생성하는 도구가 있고, 이 도구는 프로그래밍 언어로 스키마를 구현한 클래스를 생성한다. 애플리케이션 코드는 생성된 코드를 호출해 스키마의 레코드를 부호화, 복호화할 수 있다.

스리프트는 바이너리 프로토콜, 컴팩트 프로토콜 이라는 두 가지 이진 부호화 형식이 있다.

  • 바이너리 프로토콜 필드 이름이 없고, 숫자로 된 필드 태그를 포함한다. 숫자는 스키마 정의에 포함한다.
  • 컴팩트 프로토콜 필드 타입과 태그 숫자를 단일 바이트로 줄이고 가변 길이 정수를 사용해서 부호화한다.

프로토콜 버퍼는 컴팩트 프로토콜과 비슷하다.

필드 태그와 스키마 발전

  • 스키마 발전 스키마는 필연적으로 시간이 지남에 따라 변한다.

필드 태그는 부호화된 데이터를 해석하기 위해 매우 중요하다. 부호화된 데이터는 필드 이름을 모르기 때문에 스키마에서 필드 이름은 변경할 수 있다. 그러나 필드 태그는 변경할 수 없다.

필드에 새로운 태그 번호를 부여하는 방식으로 스키마에 새로운 필드를 추가할 수 있다. 예전 코드에서 새 코드로 기록한 데이터를 읽으려는 경우에 해당 필드를 무시하면 된다. (상위 호환성 유지)

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

데이터타입과 스키마 발전

필드의 데이터타입을 변경하는 것은 가능하지만, 값이 부정확하거나 잘릴 위험이 있다.

아브로

아브로는 아파치에서 개발한 이진 부호화 형식이다.

아브로도 부호화할 데이터 구조를 지정하기 위해 스키마를 사용하며, 두 개의 스키마 언어가 있다. (사람이 편집할 수 있는 IDL, 기계가 쉽게 읽을 수 있는 JSON 기반 언어)

스키마에 태그 번호가 없으며, 바이트열에 필드나 데이터타입을 식별하기 위한 정보가 없다.

아브로를 이용해 이진 데이터를 파싱하려면 스키마에 나타난 순서대로 필드를 살펴보고 스키마를 이용해 각 필드의 데이터타입을 미리 파악해야 한다. 즉, 읽기와 쓰기 간 스키마가 같아야 정확하게 데이터를 복호화할 수 있다.

쓰기 스키마와 읽기 스키마

  • 쓰기 스키마 데이터를 부호화할 때 사용한 스키마
  • 읽기 스키마 데이터가 복호화되길 기대하는 특정 스키마

아브로는 쓰기 스키마와 읽기 스키마가 동일하지 않아도 되고, 단지 호환 가능하면 된다. 데이터를 복호화할 때, 아브로 라이브러리는 쓰기 스키마와 읽기 스키마를 함께 살펴본 다음, 쓰기 스키마에서 읽기 스키마로 데이터를 변환해 차이를 해소한다.

스키마 발전 규칙

  • 아브로에서의 상위 호환성 새로운 버전의 쓰기 스키마와 예전 버전의 읽기 스키마를 가질 수 있음
  • 아브로에서의 하위 호환성 새로운 버전의 읽기 스키마와 예전 버전의 쓰기 스키마를 가질 수 있음

호환성을 유지하기 위해서는 기본값이 있는 필드만 추가하거나 삭제할 수 있다.

필드에 널을 허용하려면 유니온 타입을 사용해야 한다.

아브로는 타입을 변환할 수 있으므로 필드의 데이터타입 변경이 가능하다.

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

읽기는 특정 데이터를 부호화한 쓰기 스키마를 어떻게 알까?

  • 많은 레코드가 있는 대용량 파일 모두 동일한 스키마로 부호화된 수백만 개 레코드를 저장하는 용도일 때는, 파일 시작 부분에 한 번만 쓰기 스키마를 포함시키면 된다.
  • 개별적으로 기록된 레코드를 가진 데이터베이스 다양한 레코드들은 다양한 쓰기 스키마로 서로 다른 시점에 쓰여진다. 모든 부호화된 레코드의 시작 부분에 버전 번호를 포함하고, 데이터베이스에는 스키마 버전 목록을 유지하면 된다.
  • 네트워크 연결을 통해 레코드 보내기 양방향 네트워크 연결을 통해 통신할 때 연결 설정에서 스키마 버전 합의를 할 수 있다.

동적 생성 스키마

아브로 방식은 스키마에 태그 번호가 포함돼 있지 않다는 장점이 있다.

아브로는 동적 생성 스키마에 더 친숙하다. (스리프트나 프로토콜 버퍼의 설계 목표는 동적 생성 스키마가 아니었다.)

코드 생성과 동적 타입 언어

  • 스리프트와 프로토콜 버퍼는 코드 생성에 의존한다. 이는 정적 타입 언어에서 유용하다. 복호화된 데이터를 위해 효율적인 인메모리 구조를 사용하고, 데이터 구조에 접근하는 프로그램을 작성할 때 IDE에서 타입 확인 등의 도움을 받을 수 있기 때문이다. 동적 타입 프로그래밍 언어에서는 만족시킬 컴파일 시점 타입 검사기가 없어 코드를 생성하는 것이 중요하지 않다.

스키마의 장점

  • 스리프트, 프로토콜 버퍼, 아브로에서 사용하는 스키마 언어는 XML, JSON 스키마보다 간단하며 자세한 유효성 검사 규칙을 지원한다.
  • 많은 데이터 시스템이 이진 부호화를 독자적으로 구현하기도 한다. RDBMS는 쿼리를 데이터베이스로 보내고 응답을 받을 수 있는 프로토콜이 있다 (JDBC 등)
  • 이진 부호화 장점
    • 필드 이름 생략할 수 있어, 크기가 작다.
    • 스키마는 유용한 문서화 형식이다. 스키마가 최신 상태임을 확신할 수 있다.
    • 스키마 변경이 적용되기 전, 상위 호환성과 하위 호환성을 확인할 수 있다.
    • 정적 타입 프로그래밍 언어 사용자에게 스키마로부터 코드 생성하는 기능은 유용하다.

데이터플로 모드

데이터플로는 추상적인 개념으로, 하나의 프로세스에서 다른 프로세스로 데이터를 전달하는 것이다.

다음과 같은 방법들이 가장 보편적이다.

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

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

데이터베이스에 기록하는 프로세스는 데이터를 부호화하고 데이터베이스에서 읽는 프로세스는 데이터를 복호화한다.

하위 호환성과 상위 호환성은 필요하다.

필드가 추가된 경우, 예전 버전의 코드가 레코드를 읽으면 알지 못하는 필드가 존재한다. 예전 버전의 코드가 레코드를 갱신한 후, 다시 기록하면 알지 못하는 필드가 유실될 수 있어, 이 부분을 신경써야 한다.

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

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

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

보관 저장소

데이터베이스의 스냅숏을 수시로 만든다고 가정할 때, 데이터 덤프는 보통 최신 스키마를 사용해 부호화한다.

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

  • 일반적으로 클라이언트와 서버로 배치한다. 서버는 네트워크를 통해 API를 공개하고 클라이언트는 이 API로 요청을 만들어 서버에 연결할 수 있다. 서버가 공개한 API를 서비스라고 한다.
  • 여러 면에서 서비스는 데이터베이스와 유사하다. 서비스는 클라이언트가 데이터를 제출하고 질의하는 것을 허용한다. 차이는 데이터베이스는 질의 언어를 사용하여 직접 질의하고, 서비스는 서비스의 비즈니스 로직으로 미리 정해진 입력과 출력만 허용한 애플리케이션 특화 API를 공개한다.
  • 서버와 클라이언트가 사용하는 데이터 부호화는 서비스 API 버전 간 호환이 가능해야 한다.

웹 서비스

서비스와 통신하기 위한 기본 프로토콜로 HTTP를 사용할 때, 이를 웹서비스라고 한다.

웹서비스에는 REST, SOAP 등이 있다.

  • REST 프로토콜이 아니라, HTTP 원칙을 토대로 한 설계 철학 간단한 데이터 타입을 강조하고, URL을 사용해 리소스를 식별하고 캐시 제어, 인증, 콘텐츠 유형 협상에서 HTTP 기능을 사용
  • SOAP 네트워크 API 요청을 위한 XML 기반 프로토콜

SOAP은 많이 복잡하지만, RESTful API는 간단한 접근 방식을 선호한다.

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

  • RPC 모델은 원격 네트워크 서비스 요청을 같은 프로세스 안에서 특정 프로그래밍 언어의 함수나 메서드를 호출하는 것과 동일하게 사용 가능하게 해준다.
  • RPC 접근 방식은 근본적으로 결함이 있는데, 네트워크 요청은 로컬 함수 호출과는 매우 다르다는 점이다. 로컬 함수 호출은 예측이 가능하여, 제어 가능한 매개변수에 따라 성공하거나 실패한다. 네트워크 요청은 예측이 어렵다.
    • 로컬 함수 호출은 결과를 반환하거나, 예외를 내거나, 반환하지 않거나 이다. 네트워크 요청은 타임아웃으로 결과 없이 반환될 수 있다.
    • 실패한 네트워크 요청을 다시 시도할 때, 요청이 실제로는 처리되고 응답만 유실될 수 있다. 이 경우 프로토콜에 중복 제거 기법(멱등성)을 적용하지 않으면 재시도는 작업이 여러 번 수행되는 원인이 된다. 로컬 함수는 이런 문제가 없다.
    • 네트워크 요청은 함수 호출보다 훨씬 느리고 지연 시간은 매우 다양하다.
    • 네트워크로 요청하는 경우에는 모든 매개변수는 네트워크를 통해 전송할 수 있게끔 바이트열로 부호화해야 한다. 크기가 큰 객체는 문제가 될 수 있다.
    • 클라이언트와 서비스는 다른 프로그래밍 언어로 구현할 수 있다. 따라서 RPC 프레임워크는 하나의 언어에서 다른 언어로 데이터타입을 변환해야 한다.

RPC의 현재 방향

  • gRPC는 프로토콜 버퍼를 이용한 RPC 구현이다. 차세대 RPC 프레임워크는 원격 요청이 로컬 함수 호출과 다르다는 사실을 분명히 한다. gRPC는 하나의 요청과 하나의 응답뿐만 아니라 시간에 따른 일련의 요청과 응답으로 구성된 스트림을 지원한다.
  • REST 상에서 JSON과 같은 부류의 프로토콜보다 이진 부호화 형식을 사용하는 사용자 정의 RPC 프로토콜이 우수한 성능을 제공할 수 있다. 하지만 RESTful API는 실험과 디버깅에 적합하며 큰 생태계가 있다는 장점이 있다.

데이터 부호화와 RPC의 발전

발전성이 있으려면 RPC 클라이언트와 서버를 독립적으로 변경, 배포할 수 있어야 한다.

모든 서버를 먼저 갱신한 후, 모든 클라이언트를 갱신해도 문제가 없다고 가정한다. 그러면 요청은 하위 호환성만, 응답은 상위 호환성만 필요하다.

메시지 전달 데이터플로

비동기 메시지 전달 시스템

메시지를 직접 네트워크 연결로 전송하지 않고, 임시로 메시지를 저장하는 메시지 브로커(메시지 큐)나 메시지 지향 미들웨어라는 중간 단계를 거쳐 전송한다는 점은 데이터베이스와 유사하다.

  • 메시지 브로커 이용 장점
    • 메시지 브로커가 버퍼처럼 동작할 수 있어 시스템 안정성이 향상된다.
    • 메시지 유실을 방지할 수 있다.
    • 송신자가 수신자의 포트, ip를 알 필요 없다.
    • 하나의 메시지를 여러 수신자로 전송할 수 있다.
    • 논리적으로 송신자, 수신자가 분리된다.

송신 프로세스는 메시지에 대한 응답을 기대하지 않는다. 응답 전송이 가능하지만 별도 채널에서 수행된다. 이런 통신 패턴이 비동기이다.

메시지 브로커

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

분산 액터 프레임워크

액터 모델은 단일 프로세스 안에서 동시성을 위한 프로그래밍 모델이다. 스레드를 직접 처리하는 대신 로직이 액터에 캡슐화된다. 보통 각 액터는 하나의 클라이언트나 엔티티를 나타낸다. 액터는 비동기 메시지의 송수신으로 다른 액터와 통신한다.

분산 액터 프레임워크는 기본적으로 메시지 브로커와 액터 프로그래밍 모델을 단일 프레임워크에 통합한다.

0개의 댓글