Designing for Source Drift

“소스가 사람들이 일하는 스프레드시트라면, 보여준 스키마를 위해 설계하지 마라.”


핵심 개념

데이터 엔지니어링의 기본 가정 하나는 소스 스키마가 계약이라는 것이다. API는 버전을 갖고, DB는 마이그레이션을 갖고, 이벤트는 스키마 레지스트리를 갖는다. → Data Contracts, Schema Evolution

그런데 실무에서 가장 중요한 데이터 중 일부는 계약이 없는 소스에서 온다 — Google Sheet, 공유 드라이브의 CSV, 손으로 관리되는 설정 파일. 열두 명이 손으로 편집하고, 매달 탭이 늘고, 컬럼 이름이 움직이고, 몇 달 전 행이 제자리에서 수정된다.

흔한 반응 두 가지 모두 틀렸다:

  • “스프레드시트를 없애고 제대로 된 도구로 옮기자” — 비즈니스가 그것을 좋아하고 실제로 잘 작동한다. takedown을 제출하는 사람에게는 빠르고, 모두가 이미 접근권이 있고, 데이터 도구를 배울 필요가 없는 좋은 인터페이스다
  • “현재 스키마에 파이프라인을 맞추자” — 누군가 컬럼 이름을 바꾸는 첫 순간 깨진다

세 번째 길: 소스가 계속 바뀔 것을 전제로 설계하고, 방어를 인제스션 레이어에 집중시킨다.

첫 번째 결정 — log인가 mirror인가

가장 근본적인 설계 결정이고, 행이 쓰인 뒤에 바뀐다는 사실에서 강제된다.

1월에 제출이 기록된다. 3월에 누군가 1월 탭을 열어 “date item removed” 컬럼을 채운다. 한 행이, 몇 주 뒤에, 제자리에서 편집된다.

이것이 명백한 두 적재 전략을 모두 깨뜨린다:

전략문제
Append1월 제출이 매달 다시 카운트된다
Overwrite현재 상태는 얻지만 지난주 시트가 뭐라고 했는지 기록을 잃는다

mirror 선택 — 매 실행이 소스 전체를 읽고 테이블을 소스에 맞춰 재구축:

  • 새 행이 나타나고, 편집이 stale 중복 없이 제자리에 반영되고, 삭제된 행이 사라진다
  • 소스가 source of truth이므로 우리 테이블이 그런 척하기를 그만둔다

log 선택 — 시점별 이력이 비즈니스 요구사항일 때. 하지만 요청받지 않았다면 만들지 마라. mirror에서 log로 가는 업그레이드 경로는 명확하다 — sync 아래에 soft-delete 레이어(is_active, removed_at)를 두고 현재 상태를 view로 노출.

mirror의 대가와 가드레일

소스를 미러링한다는 것은 소스의 실수 삭제가 다음 실행에서 테이블의 실수 삭제가 된다는 뜻이다.

알고 받아들이되 두 가지를 걸어라:

  1. 최소 한 행이라도 읽혔을 때만 overwrite — 비어 있거나 실패한 API 응답이 데이터를 날릴 수 없게. “실제로 우리를 겁먹게 한 실패 모드가 그것이었다”
  2. 각 overwrite가 단일 원자적 트랜잭션 — 실행 중에 쿼리하는 사람이 반쯤 적재된 테이블을 결코 보지 않게. Delta/Iceberg 트랜잭션으로 여러 테이블을 함께 커밋하면 테이블들이 항상 서로 그리고 소스와 일치한다
  3. time travel이 안전망 — 행이 사라졌는데 그러면 안 됐다면 이전 버전이 있다 (point-in-time 복구; 진짜 감사 추적은 아니다)

인제스션 레이어의 세 가지 방어

각각이 소스가 경고 없이 바뀌는 특정 방식에 대응한다.

1. 무엇이 데이터인지 파이프라인이 결정한다

소스의 구조를 열거하고 패턴에 맞는 것만 유지한다 — 월 이름 regex를 통과하는 탭만 데이터, scratch 탭은 무시.

효과: 팀이 우리에게 먼저 말하지 않고 작업 영역을 추가할 수 있다. 이것이 핵심이다 — 비즈니스의 자유를 뺏지 않으면서 파이프라인을 안정시킨다.

2. 컬럼 이름이 움직일 것이라고 가정한다

  • 헤더를 안정적인 snake_case로 정규화 — “Date Reported”가 이번 달에 어떻게 대문자화되었든 date_reported
  • ⭐ 인식하지 못하는 것은 드롭하거나 적재를 깨뜨리게 두는 대신 단일 JSON 컬럼에 보관

두 번째가 결정적이다:

다음 달의 새 비즈니스 컬럼은 incident가 아니라 우리가 의도적으로 타입 필드로 승격시키는 대상이 된다.

드롭하면 데이터를 잃고, 실패시키면 파이프라인이 멈춘다. 보관하면 나중에 결정할 수 있다.

3. 행 fingerprint로 편집과 삽입을 구분한다

행이 어디서 왔는지(소스 ID, 탭 이름, 행 번호)와 무엇을 담고 있는지에 대한 SHA-256 해시.

  • 같은 내용의 같은 행은 항상 같은 fingerprint → 쓰기 전에 중복 제거 → Idempotency
  • 셀 하나가 바뀌면 fingerprint가 바뀐다 → 1월 행이 추가가 아니라 편집되었음을 안다

타입 캐스팅을 미뤄라

bronze는 모든 필드가 STRING, 소스에서 나온 그대로. → 이상한 값이 적재를 실패시킬 수 없다.

타이핑은 silver에서 — 날짜를 DATE로 (⚠️ 누군가 3/14/2026을 타이핑했든 Google이 그날을 serial number로 저장했든 둘 다 처리해야 한다), count를 BIGINT로.

이것이 Medallion Architecture의 bronze/silver 구분이 정형 소스보다 messy 소스에서 더 값을 하는 이유다. 정형 소스에서 bronze는 종종 형식적이지만, 손으로 편집되는 소스에서는 bronze가 “적재는 절대 실패하지 않는다”는 보증 그 자체다.

⭐ 하나의 shape, 여러 소스

가장 이전 가능한 조직적 교훈이다.

같은 bronze/silver/tracking 테이블, 같은 명명 규칙, 같은 secret 처리 패턴을 다른 소스에도 그대로 쓴다.

이것은 정돈 그 자체를 위한 것이 아니다. 리포팅이 각자 자기 API·파일 포맷·오너를 가진 여섯 개 남짓 소스에서 온다. 모두가 맞춤 아키텍처를 얻으면 파이프라인 여섯 개를 유지해야 한다. 하나의 shape를 공유하면 하나의 패턴과 하나의 운영 플레이북을 유지하고, on-call인 사람이 한 번도 본 적 없는 파이프라인을 읽을 수 있다.

messy 소스일수록 이 규칙이 중요하다 — 각 소스의 특이함이 이미 충분하므로 그 위의 구조까지 제각각이면 운영이 불가능해진다.

Forward Deployed Engineering 맥락

이 패턴이 나온 조직 형태도 관련이 있다. FDE 팀은 비즈니스 팀에 embed되어 무언가를 설계하기 전에 일을 shadow하고 수동 부분을 덜어낸다. 보통 문제에 붙는 첫 엔지니어다.

임무의 이중성이 설계를 규정한다:

일을 빠르게 돌아가게 만들되, 파일럿이 비즈니스가 의존하는 시스템이 될 때도 버티도록 만드는 것

“design for drift”는 그 이중성에 대한 답이다 — 지금 당장의 소스에 맞추면 빠르지만 버티지 못하고, 소스를 바꾸라고 요구하면 버티지만 빠르지 않다.

체크리스트

  • log인가 mirror인가를 명시적으로 결정했는가 (기본값으로 흘러가지 않았는가)
  • 빈 응답이 데이터를 지우지 못하게 막았는가
  • 쓰기가 원자적 트랜잭션인가
  • 무엇이 데이터인지 패턴으로 필터하는가 (allowlist가 아니라)
  • 모르는 컬럼을 보관하는가 (드롭·실패가 아니라)
  • 행 fingerprint로 편집과 삽입을 구분하는가
  • bronze가 전부 STRING이라 적재가 실패할 수 없는가
  • 같은 조직의 다른 messy 소스와 같은 shape를 쓰는가

연관 개념


Source: When the Source of Truth Is a Google Sheet