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

[DDIA] 4장. 부호화와 발전 (Encoding and Evolution)
목표: 시간이 지나며 시스템/스키마/코드가 바뀌어도 데이터가 깨지지 않게 설계하는 기준 잡기
키워드: 부호화(encoding), 스키마 진화(schema evolution), 호환성(backward/forward), 데이터플로우(DB/서비스/메시지)
0. 이 장이 던지는 질문
소프트웨어는 계속 바뀐다. 문제는 “코드만” 바뀌는 게 아니라, 그 코드가 다루는 데이터도 같이 움직인다는 점이다.
- 예전 버전이 저장한 데이터를 최신 버전이 읽을 수 있는가?
- 최신 버전이 만든 메시지를 예전 소비자가 처리할 수 있는가?
- 서비스 A/B가 서로 다른 버전으로 롤링 배포 중일 때도 안전한가?
결론: 시스템이 커질수록 핵심 난이도는 “기능 추가”가 아니라 호환성 유지다.
1. 부호화(Encoding)란 무엇인가?
데이터는 보통 3가지 형태로 흐른다.
- 메모리 내부 표현: 언어의 객체/구조체/클래스
- 저장 표현: 디스크(DB, 파일)에 기록되는 바이트
- 전송 표현: 네트워크(RPC, HTTP, 메시지 큐)로 이동하는 바이트
여기서 “부호화”는 (1)의 구조를 (2)/(3)의 바이트로 변환하고 다시 복원하는 과정이다.
즉, 단순 변환이 아니라 “서로 다른 시스템/버전/언어 사이의 계약”에 가깝다.
2. 진짜 어려운 건 “발전(Evolution)”이다
시간이 지나면 반드시 데이터 구조가 바뀐다.
- 필드 추가/삭제
- 타입 변경 (int → string 등)
- 이름 변경
- 중첩 구조 변경
- 의미 변경(같은 필드가 다른 뜻을 갖게 됨)
이때 목표는 하나:
- 기존 데이터/기존 소비자와 깨지지 않게 공존하기
이를 정리하는 대표 개념이 호환성이다.
2.1 호환성 용어 정리
- Backward compatibility(하위 호환): 새 코드가 예전 데이터/메시지를 읽을 수 있음
- 예: 새 버전 서버가 이전 버전 앱에서 온 요청을 처리
- Forward compatibility(상위 호환): 옛 코드가 새 데이터/메시지를 어느 정도 처리(또는 무시)할 수 있음
- 예: 롤링 배포 중 이전 버전 소비자가 새 버전 생산자의 메시지를 받아도 치명적 실패는 안 남
실무에서는 롤링 업그레이드 때문에 “일정 기간 구버전과 신버전이 섞여 공존”하는 상황이 흔해서 둘 다 중요해진다.
3. 언어 종속 직렬화의 함정 (특히 객체 직렬화)
언어 내장 직렬화(예: 특정 언어의 객체 직렬화)는 편해 보이지만, 보통 아래 문제가 크다.
- 다른 언어/플랫폼과 호환이 어려움
- 클래스 구조 변경에 취약(필드 추가/삭제/상속 구조 변화)
- 보안 이슈(역직렬화 취약점)로 이어질 수 있음
- 장기 보관 데이터(아카이빙)에 부적합
장기적으로 “서비스 경계 밖”으로 나가는 데이터는 언어 독립적인 형식을 쓰는 편이 안전하다.
4. 텍스트 기반 포맷: JSON / XML / CSV
4.1 장점
- 사람이 읽고 디버깅하기 쉬움
- 언어/플랫폼 지원 폭이 넓음
- 웹/HTTP API에 자연스럽게 녹아듦
4.2 단점 (진화 관점에서 특히)
- 타입이 모호해지기 쉬움(숫자/문자/날짜 등)
- 스키마가 “명시적 강제”가 아니라 관습/문서에 머무르기 쉬움
- 크기/속도 면에서 비효율적인 경우가 많음
- CSV는 구조(중첩/배열/필드 의미)를 표현하기 더 어려움
그래도 많이 쓰는 이유는 단순함과 생태계다.
대신 “호환성 규칙”을 팀 단위로 강하게 운영해야 한다.
5. 이진 포맷 + 스키마: Thrift / Protocol Buffers / Avro
텍스트 포맷의 단점을 보완하려고 스키마 기반 이진 부호화가 널리 쓰인다.
공통적인 목표:
- 데이터 크기 감소, 파싱 효율 증가
- 스키마로 구조를 명확히
- 스키마 진화를 체계적으로 지원
5.1 Protobuf / Thrift 계열에서 중요한 규칙(감각적으로)
- 필드에 “번호(tag)” 같은 안정적인 식별자가 있고, 이게 호환성의 핵심이 됨
- 일반적으로 안전한 변경:
- 새 필드 추가(대개 optional + default)
- 알 수 없는 필드는 무시(구버전이 신필드를 못 알아도 치명적 실패 없이 패스)
- 위험한 변경:
- 기존 필드 번호 재사용/의미 변경
- 타입 변경(호환 규칙을 깨기 쉬움)
- 필드 삭제 후 같은 번호를 다른 의미로 재사용
핵심은 “이전 소비자가 모르는 필드를 무시할 수 있어야 forward 호환이 성립”한다는 점.
5.2 Avro의 큰 특징(감각적으로)
- 스키마를 적극적으로 사용하고, writer/reader 스키마 간 해석 규칙으로 진화를 지원
- 배치/데이터 파이프라인(파일, 카프카 등)에서 스키마 레지스트리와 함께 자주 사용
어떤 포맷이든, 결국 성공의 조건은 “스키마 진화 규칙을 조직적으로 지키는가”다.
6. 데이터플로우별 호환성 포인트 (이 장의 핵심 정리)
6.1 데이터베이스를 통한 데이터플로우
- DB에는 과거 데이터가 오래 남는다.
- 애플리케이션은 롤링 배포로 점진적으로 바뀐다.
- 따라서 “새 코드가 옛 데이터를 읽고, 새 형식으로 다시 쓸 수도 있는” 상황이 자연스럽게 발생한다.
실무적 함의:
- 마이그레이션을 한 번에 끝내기 어렵다(점진적 마이그레이션 필요)
- 새/옛 스키마가 공존하는 기간을 고려해야 한다
6.2 서비스 호출(RPC/HTTP)을 통한 데이터플로우
- 클라이언트/서버의 버전이 다를 수 있음(특히 모바일 앱)
- API 계약이 곧 호환성의 핵심
실무적 함의:
- “필드 추가는 대체로 안전, 필드 삭제/의미 변경은 위험”이라는 규칙이 강해진다
- 버전 전략(URI 버전, 헤더 버전, 필드 기반 진화)을 명확히 가져가야 한다
6.3 비동기 메시징(큐/스트림)을 통한 데이터플로우
- 생산자/소비자가 시간적으로 분리되어 있고, 소비자 업데이트가 늦을 수 있다
- 메시지는 “저장된 채로” 나중에 소비될 수 있다
실무적 함의:
- forward/backward 호환을 모두 강하게 요구하는 경우가 많다
- 스키마 레지스트리/호환성 검사 같은 운영 체계가 특히 중요해진다
7. “진화 가능한 스키마”를 위한 실전 규칙
아래 규칙은 포맷/DB/메시징을 가리지 않고 잘 통한다.
- 필드는 가능한 “추가” 중심으로 진화
- 제거가 필요하면 즉시 삭제보다 “deprecated(무시) → 충분한 기간 후 제거”
- 필드 의미를 바꿔야 하면 새 필드를 만들고 점진적으로 이동
- 기본값/옵셔널 처리로 구버전이 깨지지 않게
- 알 수 없는 필드를 무시할 수 있도록 설계(또는 엄격 스키마면 버전 분리)
- 배포는 “혼재 기간”을 가정하고, 혼재 상태에서도 동작하는지 테스트
8. 내가 적용해볼 체크리스트
- 지금 내 시스템에서 데이터는 DB/HTTP/메시지 중 어디로 흐르는가?
- 롤링 배포 중 구버전/신버전이 섞여도 안전한가?
- “필드 삭제/의미 변경”이 필요한 경우의 표준 절차가 있는가?
- 이벤트/메시지 포맷에 대한 스키마(및 호환성 검사)가 있는가?
- 장기 보관 데이터에 언어 종속 직렬화를 쓰고 있진 않은가?
9. 5줄 요약
- 부호화는 데이터가 메모리/저장/전송을 오갈 때의 “계약”이다.
- 진짜 난이도는 시간이 지나며 데이터 구조가 바뀌는 “발전”에서 나온다.
- 호환성은 backward(새 코드가 옛 데이터 처리)와 forward(옛 코드가 새 데이터 무시/처리)로 정리된다.
- JSON 같은 텍스트 포맷은 단순하지만 스키마/타입/진화 규칙을 잘 운영해야 한다.
- 스키마 기반 이진 포맷은 효율과 진화에 유리하지만, 결국 핵심은 “호환성 규칙을 지키는 운영 체계”다.
Leave a comment