3 minute read

프로그램은 두 가지 방식의 표현으로 데이터를 처리한다.

  1. 메모리에서 데이터는 객체, 구조체, 리스트, 배열, 해시 테이블, 트리 등에 보관된다(포인터 사용).
  2. 파일에 데이터를 쓰거나 네트워크를 통해 데이터를 전송하려면 데이터를 일종의 독립된 바이트 시퀀스(JSON 문서)로 인코딩해야 한다.

포인터는 다른 프로세스에서는 의미가 없기 때문에, 바이트 시퀀스 표현은 메모리에서 사용되는 데이터 구조와는 상당히 다르다.

두 프로세스 사이에는 일종의 변환이 필요하다. 인메모리 표현에서 바이트 시퀀스로의 변환을 인코딩(직렬화 또는 마샬링이라고도 함)이라고 하며, 그 반대의 경우를 디코딩(구문 분석, 역직렬화, 언마샬링)이라고 한다.

언어별 형식

프로그래밍 언어에는 인메모리 객체를 바이트 시퀀스로 인코딩하는 기능이 내장되어 있다. 예를 들어:

  • Java의 java.io.Serializable
  • Ruby의 Marshal
  • Python의 pickle

이러한 인코딩 라이브러리는 최소한의 추가 코드로 인메모리 객체를 저장하고 복원할 수 있기 때문에 매우 편리하다.
그러나 이러한 인코딩 라이브러리에는 여러 가지 심각한 문제도 있다:

  • 인코딩은 특정 프로그래밍 언어에 묶여 있는 경우가 많다. 다른 언어로 데이터를 읽는 것은 매우 어렵다. 이러한 인코딩으로 데이터를 저장하거나 전송하면 현재 프로그래밍 언어에 종속되어 다른 언어를 사용할 수 있는 다른 조직의 시스템과 통합할 수 없다.
  • 공격자가 애플리케이션에서 임의의 바이트 시퀀스를 디코딩할 수 있다. 동일한 객체 유형의 데이터를 복원하려면 디코딩 프로세스에서 임의의 클래스를 인스턴스화할 수 있어야 하기 때문이다.
  • 데이터 버전 관리가 어렵다. 데이터를 빠르고 쉽게 인코딩하기 위한 것이기 때문에 이전 버전과의 호환성이라는 불편한 문제를 무시하는 경우가 많다.
  • 효율성이 낮다(인코딩 또는 디코딩에 소요되는 CPU 시간, 인코딩된 구조의 크기). 예를 들어 Java의 기본 제공 직렬화는 성능이 나쁘기로 악명이 높다.

이러한 이유로 일반적으로 언어의 내장 인코딩을 일시적인 목적 이외의 용도로 사용하는 것은 좋지 않은 생각이다.

JSON, XML 및 바이너리 변형

많은 프로그래밍 언어에서 쓰고 읽을 수 있는 표준화된 인코딩으로 이동하면 JSON과 XML이 확실한 경쟁자이다.

  • JSON의 인기는 주로 웹 브라우저에 내장된 지원과 XML에 비해 단순하다.
  • CSV는 언어에 구애받지 않는 또 다른 인기 형식이다.

JSON, XML, CSV는 텍스트 형식이므로 사람이 어느 정도 읽을 수 있지만 표면적인 구문 문제 외에도 몇 가지 미묘한 문제가 있다.

  • XML과 CSV는 숫자를 구분할 수 없다. JSON은 문자열과 숫자를 구분하지만 정수와 부동 소수점 숫자를 구분하지 않으며 정밀도를 지원하지 않는다. 예를 들어, 트위터의 API에서 반환되는 JSON에는 JavaScript 애플리케이션에서 숫자를 올바르게 구문 분석하지 못하는 문제를 해결하기 위해 트윗 ID가 두 번, 한 번은 JSON 숫자로, 한 번은 십진수 문자열로 포함된다.
  • JSON과 XML은 유니코드 문자 문자열(사람이 읽을 수 있는 텍스트)을 잘 지원하지만 바이너리 문자열(문자 인코딩이 없는 바이트 시퀀스)은 지원하지 않는다. 바이너리 문자열은 유용하기 때문에 사람들은 Base64를 사용하여 바이너리 데이터를 텍스트로 인코딩하여 이 제한을 극복한다. 그런 다음 스키마를 사용하여 값이 Base64로 인코딩된 것으로 해석된다. 이 방법은 다소 해킹이 쉽고 데이터 크기가 33% 증가한다.
  • 데이터(예: 숫자 및 이진 문자열)의 올바른 해석은 스키마의 정보에 따라 달라진다. XML/JSON 스키마를 사용하지 않는 애플리케이션은 대신 적절한 인코딩/디코딩 로직을 하드코딩해야 할 가능성이 높다.
  • CSV에는 스키마가 없으므로 각 행과 열의 의미를 정의하는 것은 애플리케이션에 달려 있다. 애플리케이션 변경으로 인해 새로운 행이나 열이 추가되면 해당 변경 사항을 수동으로 처리해야 한다.

이러한 문제점에도 불구하고 JSON, XML, CSV는 많은 용도로 충분히 사용할 수 있다. 특히 데이터 교환 형식, 즉 한 조직에서 다른 조직으로 데이터를 전송할 때 유용하다.

바이너리 인코딩

조직 내부에서만 사용되는 데이터는 공통 인코딩 형식을 사용한다. 예를 들어, 더 압축되거나 구문 분석이 빠른 형식을 선택할 수 있다. 테라바이트 단위의 원활한 통신은 데이터 형식 선택이 큰 영향을 미칠 수 있다.

JSON은 XML보다 덜 장황하지만, 두 형식 모두 바이너리 형식에 비해 여전히 많은 공간을 사용한다. 이 문제를 극복하기 위해 JSON(MessagePack, BSON, BJSON, UBJSON, BISON, Smile 등)과 XML(WBXML, Fast Infoset 등)을 위한 다양한 바이너리 인코딩이 개발되어 있다.
이러한 형식 중 일부는 데이터 유형을 확장하지만(예를 들어, 정수와 부동 소수점 숫자를 구분하거나 이진 문자열에 대한 지원 추가), 그 외에는 JSON/XML 데이터 모델을 변경하지 않고 유지한다. 특히 스키마를 규정하지 않기 때문에 인코딩된 데이터 내에 모든 객체 필드 이름을 포함해야 한다.

다음의 JSON 문서를 바이너리 인코딩할 때 사용자 이름, 즐겨찾기 번호 및 관심사 문자열을 어딘가에 포함해야 한다.

{
  "userName": "Martin",
  "favoriteNumber": 1337,
  "interests": ["daydreaming", "hacking"]
}

JSON의 바이너리 인코딩인 MessagePack의 예를 살펴보자.
다음 그림은 위의 JSON 문서를 MessagePack으로 인코딩할 때 얻을 수 있는 바이트 시퀀스를 보여준다.

MessagePack
MessagePack 인코딩

처음 몇 바이트는 다음과 같다:

  1. 첫 번째 바이트인 0x83은 다음에 오는 것이 3개의 필드(하단 4비트 = 0x03)를 가진 객체(상단 4비트 = 0x80)임을 나타낸다.
  2. 두 번째 바이트인 0xa8은 다음 내용이 8바이트 길이의 문자열(상단 4비트 = 0xa0)임을 나타낸다(하단 4비트 = 0x08).
  3. 다음 8바이트는 ASCII로 된 필드 이름 userName이다. 앞서 길이가 표시되었으므로 문자열이 끝나는 위치(또는 이스케이프)를 알려주는 마커가 필요하지 않다.
  4. 다음 7바이트는 접두사 0xa6을 사용하여 6글자 문자열 값 Martin을 인코딩한다.

바이너리 인코딩의 길이는 66바이트로, 텍스트 JSON 인코딩이 사용하는 81바이트(공백 제거)보다 약간 적다. JSON의 모든 바이너리 인코딩은 이 점에서 비슷하다. 이러한 작은 공간 감소와 구문 분석 속도 향상이라느 이점이 사람이 읽을 수 있는 가독성의 손실을 감수할 가치가 있을까?