[데이터 중심 애플리케이션 설계] 04장 디테일

book-cover

[DDIA] 4장 디테일. 부호화와 발전 (Encoding and Evolution)

이 글은 4장 요약 글의 확장판(디테일 편) 이다.
요약에서 “호환성(backward/forward) + 데이터플로우(DB/서비스/메시지)”의 큰 흐름을 잡았다면, 여기서는 실제로 시스템이 깨지는 지점을 중심으로 더 깊게 파고든다.

  • “호환성”은 왜 배포 방식(롤링 업그레이드)과 결합되는 순간 급격히 어려워질까?
  • 텍스트 포맷(JSON/CSV)은 왜 타입/스키마/의미에서 자주 터질까?
  • Protobuf/Thrift는 왜 tag(필드 번호) 를 건드리면 재앙이 될까?
  • Avro는 왜 “writer/reader 스키마 해석”을 전제로 진화를 설계했을까?
  • DB/RPC/메시징 각각에서 “안전하게 진화”시키는 패턴은 무엇일까?

0. 이 장이 진짜로 말하는 것: “변경은 피할 수 없고, 깨짐은 설계로 막는다”

시스템은 계속 바뀐다.

  • 기능 추가 → 필드 추가
  • 정책 변경 → 필드 의미 변경
  • 성능 개선 → 저장 형식 변경
  • 구조 개편 → 서비스 분리/통합

문제는 변경이 ‘동시에’ 일어나지 않는다는 점이다.

  • 롤링 배포 중: 신버전/구버전이 공존
  • 소비자 업데이트 지연: 생산자만 먼저 바뀜
  • 오래된 데이터: 몇 년 전 형식이 DB/로그/백업에 남아 있음

결국 핵심은 “구버전과 신버전이 섞여도, 데이터가 깨지지 않게 운영하는 규칙”이다.


1. 부호화(Encoding)를 “계약”으로 바라보기

1.1 데이터는 3개의 표현을 오간다

  1. 메모리 표현: 언어의 객체/구조체/클래스
  2. 저장 표현: DB/파일에 저장되는 바이트
  3. 전송 표현: 네트워크(RPC/HTTP/메시지)로 오가는 바이트

부호화/복호화는 단순 변환이 아니라, “서로 다른 코드/버전/언어가 합의한 계약”이다.

  • 생산자(serialize)가 바이트를 만들고
  • 소비자(deserialize)가 그 바이트를 해석한다

이때 “해석 규칙(스키마)”이 흐려지면, 데이터는 같은 바이트라도 다른 의미가 된다.


2. 호환성(Compatibility)의 디테일: “혼재 기간”을 기준으로 잡아야 한다

2.1 용어를 한 번 더 단단히

  • Backward compatibility(하위 호환)
    새 코드(consumer)가 옛 데이터/메시지를 읽는다.
  • Forward compatibility(상위 호환)
    옛 코드(consumer)가 새 데이터/메시지를 어느 정도 처리하거나(또는 무시하고) 살아남는다.
  • Full compatibility(완전 호환)
    위 둘을 동시에 만족(혼재 기간이 길고 복잡할수록 필요).

중요한 관점은 이거다.

호환성은 “파일 포맷” 문제가 아니라 “배포/운영 방식(롤링 업그레이드)” 문제와 합쳐질 때 폭발한다.

2.2 롤링 업그레이드가 만든 현실

롤링 배포 중에는 이런 상태가 자연스럽게 생긴다.

  • 생산자(신버전) → 소비자(구버전)
  • 소비자(신버전) → 생산자(구버전)
  • DB에는 구버전이 만든 레코드가 계속 남아 있음

따라서 “한쪽만 맞추면 된다”가 아니라, 혼재 기간을 견디는 규칙이 필요하다.


3. 언어 종속 직렬화(객체 직렬화)의 함정: 편함의 대가가 크다

언어 내장 직렬화(예: Java Serialization, Python pickle 등)는 빠르게 구현할 때 유혹적이다.
하지만 DDIA가 경고하는 포인트는 장기적으로 치명적이다.

  • 언어/플랫폼 종속: 다른 언어 서비스가 끼면 호환이 매우 어려움
  • 스키마 진화에 취약: 클래스 구조 변경(필드/상속/패키지 경로)이 바로 깨짐으로 이어짐
  • 보안 리스크: 역직렬화 공격(임의 코드 실행 등)으로 이어질 수 있음
  • 아카이빙 부적합: “몇 년 뒤” 읽으려면 런타임/클래스/버전까지 맞춰야 할 수 있음

서비스 경계를 넘거나(네트워크), 오래 보관할 데이터라면 “언어 독립 포맷 + 스키마 진화 규칙”이 기본값이다.


4. 텍스트 포맷(JSON/XML/CSV) 딥다이브: “타입/의미”가 흔들린다

4.1 장점은 분명하다

  • 사람이 읽을 수 있어 디버깅이 쉽다
  • 지원 생태계가 넓다(HTTP API와 자연스럽다)
  • 도구/로그/관측이 편하다

4.2 그런데 진화 관점에서 자주 터지는 포인트

(1) 숫자/정밀도/날짜는 포맷이 아니라 “해석” 문제

JSON 예시만 봐도:

  • 정수 vs 실수 구분, 큰 정수 정밀도(언어별 처리 차이)
  • timestamp를 문자열로 둘지 숫자로 둘지, timezone을 포함할지
  • null과 “필드 없음(absent)”의 의미 차이

결국 “스키마가 없다면”, 서로 다른 팀/서비스가 제각각 해석하기 쉽다.

(2) 스키마가 문서에만 있으면, 운영 중에 틀어진다

문서(OpenAPI/위키)는 중요하지만, “실제 바이트”를 강제하지 못한다.
현실에서는 다음이 반복된다.

  • 생산자 쪽에서 필드 의미를 슬쩍 바꾼다
  • 소비자는 문서를 안 보고 그대로 파싱한다
  • 특정 케이스에서만 깨져서 늦게 발견된다(p99, 특정 고객 데이터)

텍스트 포맷을 쓰더라도, ‘호환성 규칙’을 코드/테스트/검증으로 강제해야 한다.

(3) CSV는 특히 “의미”가 약하다

  • 중첩 구조/배열 표현이 빈약
  • 컬럼 순서/헤더/구분자/인코딩 변화에 취약
  • 타입 정보가 사실상 없다(모두 문자열처럼 굴러가기 쉽다)

5. 스키마 기반 이진 포맷(Protobuf/Thrift): “tag가 곧 계약”이다

텍스트 포맷은 유연하지만 모호해지기 쉽고, 대규모/고성능/다언어 환경에서는 이진 포맷이 강해진다.
다만 이진 포맷의 핵심은 “압축/속도”보다 스키마 진화 규칙이다.

5.1 Protobuf/Thrift 계열의 호환성 감각

  • 필드에는 안정적인 식별자(보통 필드 번호/tag)가 붙는다.
  • 구버전은 모르는 필드가 들어오면 무시할 수 있어야 한다.
  • 새 필드를 추가할 때는 “기본값/옵셔널” 설계가 중요하다.

안전한 변경(대체로)

  • 새 필드 추가(옵셔널/기본값)
  • 소비자가 읽지 않는 필드를 추가(구버전은 무시)

위험한 변경(대체로)

  • 기존 필드의 tag 재사용
  • 필드 의미를 바꿨는데 tag는 유지(겉보기엔 호환인데 의미가 깨짐)
  • 타입 변경(호환 규칙을 깨기 쉬움)
  • 삭제 후 같은 tag를 다른 의미로 재사용

핵심: “tag/번호는 한 번 배포되면 영구 식별자”처럼 다뤄야 한다.

5.2 ‘필드 삭제’는 실제로는 “예약(reserve) + 폐기(deprecate)”에 가깝다

실무적으로는 이런 흐름이 안전하다.

  1. 새 필드 추가(대체 경로 제공)
  2. 구 필드 deprecated 처리(읽기는 하되 쓰기는 중단)
  3. 충분한 혼재 기간 이후 구 필드 제거
  4. 제거한 tag/이름은 재사용하지 않고 예약(같은 번호가 다른 의미로 부활하는 사고 방지)

6. Avro 딥다이브: “writer/reader 스키마 해석”을 설계에 포함한다

Avro는 Protobuf/Thrift와 결이 다르다.
특히 데이터 파이프라인(파일/카프카/배치 처리)에서 자주 등장하는 이유가 있다.

6.1 Avro의 핵심 아이디어(감각)

  • 데이터는 “어떤 스키마로 썼는지(writer schema)”가 중요하다.
  • 읽는 쪽은 “내가 기대하는 스키마(reader schema)”로 해석한다.
  • 둘 사이의 해석/해결 규칙(schema resolution) 으로 진화를 지원한다.

즉, Avro는 “스키마가 바뀌는 게 정상”이라는 전제를 설계에 박아 넣는다.

6.2 진화 규칙이 특히 중요한 포인트

  • 새 필드 추가 시 기본값이 없으면 구데이터를 읽을 때 문제가 된다(구데이터에는 그 필드가 없으니까).
  • 필드 이름 변경은 ‘alias’ 같은 장치로 해결하는 식의 규칙이 필요해진다.
  • 결국 Avro도 “스키마 레지스트리/버전 관리/호환성 검사” 같은 운영 체계와 궁합이 좋다.

포맷이 답이 아니라 “스키마를 배포/검증/진화시키는 운영 체계”가 답이다.


7. 데이터플로우별 디테일: 같은 ‘진화’라도 깨지는 지점이 다르다

DDIA 4장이 좋은 이유는 “포맷 소개”에서 끝나지 않고, 데이터가 흐르는 방식에 따라 안전장치가 달라진다는 점을 짚기 때문이다.


7.1 데이터베이스를 통한 데이터플로우: “과거가 항상 남아 있다”

DB에는 과거 데이터가 오래 남는다.
애플리케이션은 롤링 배포로 점진적으로 바뀐다.

따라서 현실은 보통 이렇다.

  • 새 코드가 옛 레코드를 읽어야 한다(Backward는 거의 필수)
  • 새 코드가 새 형식으로 다시 쓰기 시작한다(혼재가 발생)
  • 마이그레이션을 “한 번에” 끝내기 어렵다

실전 패턴: Expand → Migrate/Backfill → Contract

(일명 확장-수렴 패턴)

  1. Expand(확장): 새 컬럼/새 필드 추가(구버전도 동작 가능)
  2. Migrate/Backfill(이행): 데이터 점진적 변환(백필/배치/온라인 변환)
  3. Contract(수렴): 구 필드 제거(충분한 혼재 기간 이후)

이 패턴은 “다운타임 없는 스키마 진화”에서 거의 표준으로 쓰인다.


7.2 서비스 호출(RPC/HTTP): “클라이언트가 느리게 바뀐다”

특히 모바일/외부 고객 환경은 클라이언트 업데이트가 느리다.
그래서 서버는 오래된 클라이언트를 오래 지원해야 한다.

실전 규칙(HTTP/RPC 공통)

  • 필드 추가는 대체로 안전
  • 필드 삭제/의미 변경은 위험
  • 알 수 없는 필드는 무시할 수 있어야 한다(관대한 파서)
  • 필드 순서/존재에 의존하지 않는다
  • null vs absent(없음)의 의미를 명확히 한다

버전 전략은 “기술”보다 “운영”이다

  • URI 버전(/v1/, /v2/)은 이해가 쉽지만, 버전이 늘면 운영 비용이 커질 수 있다
  • 헤더/콘텐츠 협상 방식은 유연하지만, 팀 합의가 약하면 복잡해지기 쉽다
  • 많은 경우 “버전을 폭발시키지 않도록, 필드 추가 중심으로 진화”하는 게 실전적이다

7.3 비동기 메시징(큐/스트림): “시간적으로 분리된 시스템”

메시지는 저장된 채로 나중에 소비될 수 있다.
소비자 업데이트가 느리면, 구버전 소비자가 신버전 메시지를 받는 기간이 길어진다.

따라서 메시징에서는 이런 요구가 강해진다.

  • forward/backward 호환 모두 중요
  • 스키마 레지스트리 + 호환성 검사(검증 파이프라인)가 큰 가치
  • 이벤트 포맷을 “장기 계약”으로 보고 엄격히 관리

이벤트는 특히 “의미 변경”이 치명적이다

필드 이름은 같아도, 의미가 바뀌면 데이터는 조용히 망가진다.

  • 수치 단위 변경(ms ↔ sec)
  • 상태 코드 의미 변경(0/1의 의미가 뒤집힘)
  • enum 값 추가/재정의

이런 변화는 형식상 호환이어도, 결과적으로 시스템을 깨뜨린다.


8. “진화 가능한 스키마”를 위한 실전 규칙 (포맷/DB/메시징 공통)

아래 규칙은 포맷이 JSON이든 Protobuf든 Avro든 대체로 잘 통한다.

  1. 진화는 ‘추가’ 중심으로 설계한다
  2. 제거가 필요하면 즉시 삭제 대신 deprecated → 충분한 기간 후 제거
  3. 의미를 바꿔야 하면 새 필드/새 이벤트로 분리하고 점진적 전환
  4. 기본값/옵셔널 설계를 통해 “없어도 동작”하게 만든다
  5. 소비자는 모르는 필드를 무시하고, 생산자는 불필요하게 깨지지 않는 형식을 만든다(관대한 읽기/엄격한 쓰기)
  6. 배포는 “혼재 기간”을 전제로, 혼재 상태에서의 테스트를 넣는다
  7. (이진 포맷) tag/식별자는 한 번 쓰면 재사용하지 않는다
  8. (메시징) 이벤트는 “한 번 발행되면 오래 간다”는 전제로 스키마/의미를 관리한다

9. 내가 적용해볼 체크리스트(디테일 편)

  • 내 데이터는 DB/HTTP/RPC/메시지 중 어디를 통해 흐르는가? (복수일 가능성이 높다)
  • 롤링 배포 중 구버전/신버전이 섞여도 안전한가?
  • 필드 null과 “없음(absent)”의 의미를 팀이 합의했는가?
  • “필드 삭제/의미 변경”이 필요할 때의 표준 절차가 있는가?
  • 메시지/이벤트 포맷에 스키마와 호환성 검증(테스트/CI)이 있는가?
  • 장기 보관 데이터에 언어 종속 직렬화를 쓰고 있진 않은가?
  • ‘형식 호환’ 말고 ‘의미 호환(semantic compatibility)’까지 검증하고 있는가?

10. 5줄 요약(디테일 버전)

  • 부호화는 메모리/저장/전송 표현을 잇는 “계약”이고, 진짜 난이도는 시간이 지나며 계약이 바뀌는 과정에 있다.
  • 호환성은 backward/forward로 정리되지만, 현실에서는 롤링 배포/업데이트 지연 때문에 “혼재 기간”을 버티는 규칙이 핵심이다.
  • JSON/CSV 같은 텍스트 포맷은 단순하지만 타입/의미/스키마 강제가 약해 운영 규칙과 검증이 중요하다.
  • Protobuf/Thrift는 tag(필드 번호)가 계약의 핵심이라 재사용/의미 변경이 특히 위험하다.
  • Avro를 포함해 어떤 포맷을 쓰든, 성공의 조건은 스키마 버전 관리와 호환성 검증을 “운영 체계”로 굴리는 것이다.

Leave a comment