4. Encoding and Evolution

부호화 형식과 상위·하위 호환성, 데이터플로

  • Haram Lee
  • 2026-04-12
  • studies / Topics / DDIA
  • 4장의 핵심: 데이터는 코드보다 오래 살고, 시스템에는 예전 코드와 새 코드가 동시에 공존할 수 있음. 그래서 부호화 형식이 상위 호환성과 하위 호환성을 지원해야 함
  • 즉 단순히 어떻게 저장하느냐가 아니라, 변경이 계속 일어나는 시스템에서 어떻게 안전하게 발전하느냐의 문제

4.1 Encoding and Compatibility

  • 프로그램은 메모리 안에서 객체, 구조체, 배열, 해시맵 같은 형태로 데이터를 다룸
  • 파일에 저장하거나 네트워크로 보내려면 바이트열로 바꿔야 함
    • 부호화(encoding): 메모리 표현 → 바이트열. 직렬화(serialization), 마샬링(marshalling)이라고도 함
    • 복호화(decoding): 바이트열 → 메모리 표현. 역직렬화(deserialization), 언마샬링(unmarshalling)이라고도 함
  • 문제는 시스템이 계속 변한다는 점
    • 새 기능이 생기면 필드가 추가되거나, 기존 필드 의미가 바뀌거나, 구조가 통째로 바뀔 수 있음
    • 그런데 코드를 한 번에 다 바꿀 수 없고, 서버는 rolling upgrade로 조금씩 배포됨
  • 그래서!! 중요한 개념이
    1. 하위 호환성(backward compatibility): 새 코드가 예전 데이터 형식을 읽을 수 있어야 함
    2. 상위 호환성(forward compatibility): 예전 코드가 새 데이터 형식을 읽을 수 있어야 함
  • 하위 호환성은 새 코드가 예전 형식을 알면 되니까 비교적 쉬움. 상위 호환성은 예전 코드가 모르는 새 필드를 무시할 수 있어야 해서 더 까다로움
flowchart LR
  oldData["Old Data"] -- "backward compatibility" --> newCode["New Code"]
  newData["New Data"] -- "forward compatibility" --> oldCode["Old Code"]

4.2 Language-Specific Formats

  • 많은 언어가 자체 직렬화 기능을 제공함
    • e.g. Java Serializable, Ruby Marshal, Python pickle
    • 인메모리 객체를 아주 쉽게 저장하고 복원할 수 있어서 편함
  • 하지만 문제가 많음
    1. 특정 언어에 강하게 묶임
    2. 다른 언어에서 같은 데이터를 읽기 어려움
    3. 복호화 과정에서 보안 문제가 생길 수 있음
    4. 버전 관리와 상하위 호환성을 제대로 고려하지 않는 경우가 많음
    5. 성능이나 부호화 결과 크기 면에서도 비효율적일 수 있음
  • 잠깐 쓰는 용도가 아니라 오래 가는 시스템이나 여러 서비스가 얽힌 환경에서는 일반적으로 좋지 못함…

4.3 JSON, XML, and CSV

  • 대표적인 텍스트 기반 언어 독립적 형식
  • 사람이 어느 정도 읽을 수 있고, 여러 언어와 도구에서 잘 지원됨
  • JSON은 XML보다 덜 장황하고 웹에서 특히 많이 쓰임. CSV도 단순해서 여전히 많이 쓰임

문제점

  • 숫자 표현이 애매함
    • XML과 CSV는 숫자와 숫자 문자열을 명확히 구분하기 어려움
    • JSON은 숫자와 문자열은 구분하지만 정수·부동소수점 구분과 정밀도 문제는 남음. 큰 정수는 JavaScript 같은 언어에서 정확히 표현되지 않을 수 있음
  • JSON과 XML은 이진 문자열을 직접 잘 못 다룸. 그래서 보통 Base64로 감싸 보내는데, 그러면 크기가 약 33% 늘어남
  • XML과 JSON은 스키마를 둘 수는 있지만 실제로는 강제하지 않고 쓰는 경우가 많음. CSV는 아예 스키마가 없어서 칼럼 의미를 애플리케이션이 직접 해석해야 함
  • 근데 문제가 너무 많은데요? → 그래도 사람이 읽을 수 있다는 건 중요함… 그래서 널리 쓰임…

4.4 Binary Encoding

  • 조직 내부에서만 쓰는 데이터라면 굳이 JSON 같은 최소공통분모 형식을 고집할 필요 없음. 더 작고 빠르게 파싱되는 이진 형식을 쓰면 됨
  • JSON의 이진 변형: MessagePack, BSON, Smile
    • 대부분 JSON 데이터 모델은 그대로 두고 이진화만 한 것
    • 스키마가 없으니 필드 이름을 데이터 안에 여전히 반복해서 담아야 함
  • 그래서 단순 이진 JSON은 조금 작아질 뿐 구조적으로 큰 이득은 없음. 더 큰 이득을 보려면 스키마 기반 이진 형식으로 가야 함

4.5 Thrift and Protocol Buffers

  • Apache Thrift와 Protocol Buffers: 대표적인 스키마 기반 이진 부호화 형식
    • 먼저 데이터 구조를 스키마로 정의하고, 그걸로 여러 언어용 코드를 생성해서 씀
  • 핵심은 필드 이름 대신 숫자 태그(field tag)를 데이터에 넣는다는 점
    • userName, favoriteNumber 같은 문자열을 반복 저장하지 않아도 돼서 훨씬 짧음
  • 정수는 보통 가변 길이 정수(variable-length integer)로 부호화하므로 작은 숫자는 더 적은 바이트를 씀
  • 즉 “이진"이라는 점보다 스키마를 알고 있어서 불필요한 메타데이터를 줄일 수 있다는 게 더 중요
flowchart LR
  json["JSON<br/>{&quot;userName&quot;: &quot;...&quot;}<br/>필드 이름을 매번 저장"] --> pb["Protocol Buffers<br/>tag 1 + type + value<br/>이름 대신 태그 번호"]

스키마 발전 규칙

  • 필드 태그 번호는 바꾸면 안 됨. 이름은 바꿔도 되지만, 태그를 바꾸면 예전 데이터가 어떤 필드인지 해석할 수 없음
  • 필드 추가: 새 태그 번호를 부여
    • 예전 코드는 모르는 필드를 무시하므로 상위 호환
    • 새 코드는 예전 필드를 그대로 이해하므로 하위 호환
  • 단, 새 필드를 required로 추가하면 예전 데이터엔 그 값이 없어서 깨짐. 그래서 새 필드는 optional이거나 기본값이 있어야 함
  • 필드 삭제: 보통 optional 필드만 안전하게 지울 수 있고, 한 번 쓴 태그 번호는 재사용하면 안 됨
  • 데이터타입 변경: 불가능하진 않지만 값이 잘리거나 부정확해질 수 있음
    • e.g. 32비트 → 64비트 정수: 새 코드는 예전 데이터를 잘 읽지만, 예전 코드는 새 큰 값을 정확히 못 담을 수 있음

4.6 Avro

  • Avro도 스키마 기반 이진 형식이지만, Thrift, Protocol Buffers와 가장 큰 차이는 필드 태그 번호가 없다는 점
  • 부호화된 데이터 안에 필드 이름이나 타입 정보를 거의 넣지 않음. 읽는 쪽이 스키마를 안다고 전제하므로 같은 데이터를 더 짧게 저장함

쓰기 스키마와 읽기 스키마

  • 쓰기 스키마(writer’s schema)와 읽기 스키마(reader’s schema)를 구분
  • 둘이 완전히 같지 않아도 호환되기만 하면 읽을 수 있음
  • 읽을 때 Avro 라이브러리가 두 스키마를 같이 보고 차이를 해소함
    • 쓰기 스키마에만 있는 필드 → 무시
    • 읽기 스키마에만 있는 필드 → 기본값으로 채움
flowchart LR
  writer["Writer's Schema"] --> resolve["Schema Resolution"]
  reader["Reader's Schema"] --> resolve
  bytes["Encoded Bytes"] --> resolve
  resolve --> record["Decoded Record"]

스키마 발전 규칙

  • 기본값이 있는 필드만 안전하게 추가하거나 삭제할 수 있음
    • 기본값 없는 필드를 추가하면 예전 데이터엔 값이 없으니 하위 호환성이 깨질 수 있음
    • 기본값 없는 필드를 삭제하면 예전 코드가 기대하던 값을 못 받으니 상위 호환성이 깨질 수 있음
  • optional, required 대신 유니온 타입과 기본값을 씀
    • null을 허용하려면 유니온 안에 null을 명시해야 해서, 어떤 필드가 널일 수 있는지가 더 분명해짐

장점

  • 태그 번호를 수동 관리할 필요가 없어서 동적으로 생성되는 스키마와 잘 맞음
    • e.g. 관계형 DB 스키마에서 Avro 스키마를 자동 생성해 덤프를 만드는 식. Hadoop 같은 환경에서 특히 잘 쓰임
  • 코드 생성 없이도 쓸 수 있어서 동적 타입 언어와도 잘 맞음. 정적 타입 언어 중심인 Thrift, Protocol Buffers와 구별되는 점

4.7 The Merits of Schemas

  • Thrift, Protocol Buffers, Avro는 필드 이름을 생략하거나 최소화하니까 텍스트 형식보다 훨씬 짧음
  • 스키마 자체가 좋은 문서가 됨. 읽으려면 어차피 스키마가 필요하니 실제 구조와 문서가 따로 노는 문제가 줄어듦
  • 스키마 변경 전에 상위·하위 호환성을 미리 확인할 수 있음. 그래서 “빠르고 작다"보다 발전성과 유지보수성까지 챙긴다는 게 중요
  • 정적 타입 언어에서는 코드 생성도 큰 장점: 컴파일 시점 타입 검사, IDE 자동완성

4.8 Modes of Dataflow

  • 이 장은 부호화 형식만이 아니라 데이터가 시스템 안에서 어떻게 흐르는가도 같이 봄
    1. 데이터베이스를 통한 데이터플로
    2. 서비스를 통한 데이터플로
    3. 비동기 메시지 전달을 통한 데이터플로
  • 결국 중요한 건 누가 부호화하고 누가 복호화하는가, 그리고 서로 다른 버전의 코드가 만나도 문제가 없게 호환성을 유지하는 것
flowchart TD
  p["Process A (encode)"] --> db[("Database")]
  db --> q["Process B (decode)"]
  p --> svc["Service: REST / RPC"]
  svc --> q
  p --> broker["Message Broker"]
  broker --> q

4.9 Dataflow Through Databases

  • DB에서는 쓰는 프로세스가 부호화하고, 읽는 프로세스가 복호화함
    • DB에 값을 저장하는 건 일종의 “미래의 자신에게 메시지를 보내는 것”
  • 당연히 하위 호환성이 필요함. 아니면 예전에 저장한 데이터를 나중 코드가 못 읽음
  • 그런데 여러 버전의 애플리케이션 인스턴스가 동시에 DB를 읽고 쓰므로 상위 호환성도 필요함
  • 추가 문제: 새 필드를 예전 코드가 날려버릴 수 있음
    • 새 코드가 새 필드를 저장 → 예전 코드가 그 레코드를 읽고 다시 저장 → 모르는 필드가 사라짐
    • 애플리케이션 수준에서 알 수 없는 필드를 보존해야 함
  • 데이터가 코드보다 오래 산다(data outlives code)
    • 코드는 몇 분 만에 새 버전으로 갈아끼울 수 있지만 데이터는 몇 년씩 남음
    • 그래서 DB 하나 안에 서로 다른 시점, 서로 다른 스키마 버전의 데이터가 섞여 있음
  • 가능하면 전체 데이터를 매번 새 형식으로 다시 쓰지 않고도 읽을 수 있어야 함
    • 스키마 발전은 여러 버전 데이터가 섞여 있어도 시스템 전체는 하나의 일관된 스키마를 쓰는 것처럼 보이게 만드는 기술
  • 백업이나 데이터 웨어하우스 적재용 덤프는 보통 최신 스키마로 다시 부호화함. 이때는 Avro 객체 컨테이너 파일이나 분석 친화적인 Parquet 같은 형식이 적합

4.10 Dataflow Through Services: REST and RPC

  • 네트워크로 통신하는 프로세스는 보통 클라이언트-서버 구조. 서버가 API를 제공하고 클라이언트가 호출함. 여기서도 한쪽이 부호화하고 다른 쪽이 복호화함
  • 서비스는 DB와 비슷하게 데이터를 주고받지만 임의 질의를 허용하지 않음
    • 미리 정해진 입력과 출력만 허용함. 일종의 캡슐화로, “무엇을 할 수 있는가"를 더 좁고 명확하게 제어
  • HTTP를 기본 프로토콜로 쓰는 서비스를 웹 서비스라 함. 대표적으로 REST와 SOAP
  • REST
    • 프로토콜이라기보다 HTTP 원칙을 활용한 설계 철학
    • URL로 리소스를 식별하고, 캐시, 인증, 콘텐츠 협상 같은 HTTP 기능을 적극 활용
    • 비교적 단순하고 공개 API나 마이크로서비스 맥락에서 널리 쓰임
  • SOAP
    • XML 기반 프로토콜. WSDL로 API를 기술하고 코드 생성과 도구 지원에 많이 의존
    • 기능은 많지만 복잡하고 무거워서 지금은 대체로 REST보다 덜 선호됨
  • 정리: REST는 단순성과 생태계, SOAP은 강한 표준화와 도구 지원이 강점이지만, 실제로는 REST가 더 가볍고 다루기 쉬워서 많이 쓰임

4.11 The Problems with Remote Procedure Calls

  • RPC(Remote Procedure Call): 원격 호출을 로컬 함수 호출처럼 보이게 만들려는 아이디어. 편해 보이지만 네트워크 호출은 로컬 호출과 근본적으로 다름
  • 실패 방식이 훨씬 다양하고 예측하기 어려움
    • 로컬 호출: 성공하거나, 예외를 던지거나, 아예 반환하지 않거나
    • 네트워크 요청: 요청이나 응답 유실, 타임아웃, 상대가 느려짐
  • 응답이 없을 때 요청이 안 간 건지, 갔는데 응답만 유실된 건지 알기 어려움. 재시도하면 같은 요청이 중복 수행될 위험이 있어서 멱등성(idempotence)이 중요해짐
  • 로컬 호출보다 훨씬 느리고 지연 시간 변동도 큼
  • 매개변수를 모두 바이트열로 부호화해야 해서 객체 전달이 자연스럽지 않음
  • 클라이언트와 서버가 서로 다른 언어일 수 있어서 타입 매핑 문제도 생김
  • 그래서 네트워크 호출을 로컬 호출처럼 보이게 하는 건 한계가 명확함. 오히려 REST의 장점 하나는 네트워크 프로토콜이라는 사실을 숨기지 않는다는 것
  • 그래도 RPC는 여전히 많이 쓰임
    • gRPC, Thrift RPC, Avro RPC 등
    • future/promise, 스트리밍 같은 기능으로 네트워크 호출의 비동기성과 실패 가능성을 더 정직하게 드러내는 방향으로 발전 중
  • 보통 공개 API는 REST, 같은 데이터센터 안의 내부 서비스 간 통신은 이진 부호화 기반 RPC가 성능상 유리한 경우가 많음
  • 결국 선택은 성능, 디버깅 편의성, 생태계, 조직 내 사용 방식까지 같이 봐야 함

4.12 Message-Passing Dataflow

  • 비동기 메시지 전달(asynchronous message passing): RPC처럼 요청을 보내지만, 상대에게 직접이 아니라 메시지 브로커(message broker)를 거침
  • 브로커를 쓰면
    • 수신자가 잠시 죽어 있거나 과부하 상태여도 메시지를 버퍼링해 주므로 안정성이 높아짐
    • 메시지를 다시 전달해 유실을 줄일 수 있음
    • 송신자가 수신자의 IP 주소나 포트를 몰라도 됨
    • 메시지 하나를 여러 수신자에게 전달할 수 있음
  • 즉 브로커는 송신자와 수신자를 느슨하게 결합시킴. 송신자는 메시지를 게시만 하고 누가 소비하는지는 크게 신경 쓰지 않아도 됨
  • 메시지 전달은 보통 단방향. 보낸 쪽은 즉시 응답을 기대하지 않음. 필요하면 응답을 다른 큐나 토픽으로 보낼 수는 있음
  • 브로커는 보통 특정 데이터 모델을 강요하지 않음. 메시지는 메타데이터가 붙은 바이트열일 뿐이라, 여기서도 결국 상위·하위 호환성을 갖는 부호화 형식을 고르는 게 중요
flowchart LR
  producer["Sender"] -- "publish" --> broker["Message Broker<br/>(queue / topic)"]
  broker --> c1["Consumer 1"]
  broker --> c2["Consumer 2"]

4.13 Distributed Actor Frameworks

  • 액터 모델은 원래 단일 프로세스 안에서 동시성을 다루기 위한 프로그래밍 모델
    • 각 액터는 자신의 로컬 상태를 갖고, 다른 액터와는 비동기 메시지로만 통신
    • 한 번에 메시지 하나만 처리하므로 스레드 경쟁 조건이나 잠금을 직접 다루지 않아도 됨
  • 분산 액터 프레임워크는 이걸 여러 노드로 확장한 것
    • 같은 노드든 다른 노드든 비슷한 방식으로 메시지를 주고받게 하려 함
    • 다른 노드로 가는 메시지는 결국 네트워크를 타니까 바이트열로 부호화됨
  • 액터 모델은 원래 메시지 유실 가능성을 어느 정도 전제하므로, 위치 투명성이 RPC보다 조금 더 자연스럽게 작동함
  • 그래도 순회식 업그레이드(rolling upgrade)를 하면 예전 버전 액터와 새 버전 액터가 동시에 메시지를 주고받으므로, 여기서도 상위·하위 호환성을 신경 써야 함
Discussion