왜 나는 JSON 스키마를 신뢰하는 것을 그만두고 CI에서 데이터 계약 (Data Contracts)을 강제하기 시작했는가
요약
JSON 스키마와 수동적 레지스트리 방식의 한계를 지적하며, 데이터 무결성을 보장하기 위해 CI 파이프라인에서 데이터 계약(Data Contracts)을 강제해야 함을 강조합니다. 스키마 드리프트로 인한 운영 중단을 방지하기 위한 엔지니어링 접근법을 제안합니다.
핵심 포인트
- 수동적 스키마 레지스트리는 런타임 오류를 방지하기에 부족함
- 데이터 변경 사항을 CI 단계에서 유닛 테스트처럼 검증해야 함
- 스키마 드리프트를 방지하기 위한 데이터 계약(Data Contracts) 도입 필요
- 데이터를 단순 바이트 스트림이 아닌 버전 관리되는 API처럼 취급해야 함
스키마 레지스트리 (Schema registries)가 빌드를 중단시키지 못한다면 그것은 시간 낭비입니다.
만약 당신이 여전히 Breaking change (파괴적 변경)에 대해 다운스트림 소비자 (downstream consumers)에게 "경고"를 주기 위해 Confluent Schema Registry에 의존하고 있다면, 당신은 이미 전쟁에서 패배한 것입니다. 당신은 데이터 무결성 (data integrity)을 컴파일 전 요구 사항 (pre-compilation requirement)이 아닌, 런타임 (runtime) 시의 허가 요청으로 취급하고 있습니다. 헬스케어 및 핀테크를 위한 데이터 플랫폼을 구축해 온 6년 동안, 저는 "무해한" 필드 폐기 (field deprecations)로 인해 발생하는 수많은 운영 중단 (production outages) 사례를 목격했으며, 인간의 의사소통은 데이터 전략이 될 수 없다는 것을 깨달았습니다.
CI 파이프라인 (CI pipeline)에 포함되어 있지 않다면, 그것은 존재하지 않는 것입니다.
일반적인 접근 방식이 부족한 이유
현재 업계 표준은 "수동적 강제 (passive enforcement)"입니다. 당신은 레지스트리에 Avro 스키마를 게시하고, 만약 Kafka 프로듀서 (producers)를 올바르게 설정했다면 아마도 호환성 검사 (compatibility check)를 받을 수 있을 것입니다. 하지만 다운스트림 SQL 변환 계층 (SQL transformation layer)은 어떻게 될까요? user_id가 STRING일 것이라고 예상하는 dbt 모델이, 결제 팀의 누군가가 데이터베이스를 정리하기로 결정했다는 이유로 갑자기 BIGINT를 받게 된다면 어떻게 될까요?
수동적인 레지스트리들은 실제 코드의 라이프사이클 (lifecycle)과 분리되어 있습니다. 프로듀서는 스키마를 변경하고, 레지스트리가 200 OK를 반환할 때까지 기다린 후 서비스를 배포할 수 있습니다. 소비자(consumer)—종종 완전히 다른 팀인—는 total_amount 필드가 이제 float 대신 중첩된 객체 (nested object)가 되었기 때문에, 새벽 3시에 자신들의 Pydantic 모델이 터져버릴 때 비로소 그 사실을 알게 됩니다.
우리는 데이터를 버전 관리 (versioned)되어야 하는 API가 아니라, 변경될 수도 있는 바이트 스트림 (stream of bytes)처럼 취급합니다. 만약 당신의 데이터 파이프라인 (data pipeline)이 스키마 드리프트 (schema drift)를 실패한 유닛 테스트 (unit test)로 취급하지 않는다면, 당신은 데이터 엔지니어링 (data engineering)을 하고 있는 것이 아니라 데이터 고고학 (data archaeology)을 하고 있는 것입니다.
Photo by okeykat on Unsplash
레지스트리에서 저장소로 계약(Contract) 이동하기
저희 팀은 중앙 집중식 레지스트리 전용 워크플로우에서 '코드로서의 계약 (Contract-as-Code)' 모델로 전환했습니다. 저희는 스키마를 프로듀서(producer)의 저장소 내 YAML 또는 Protobuf 형식으로 정의하지만, 공유 모노레포(monorepo)에 contracts/ 디렉터리를 유지하고 CI 파이프라인에서 이를 참조합니다.
프로듀서 팀이 변경을 원할 때, 단순히 코드를 업데이트하는 것이 아닙니다. 그들은 계약 파일의 변경 사항을 포함하는 PR (Pull Request)을 열게 됩니다. 저희 CI 파이프라인은 data-contract-cli와 같은 도구(또는 jsonschema 및 dbt-checkpoint를 감싼 사용자 지정 래퍼)를 실행하여 새로운 스키마가 기존 다운스트림 기대치(downstream expectations)를 깨뜨리지 않는지 검증합니다.
# example-contract.yaml
dataContract: 1.0.0
servers:
...
개발자가 currency 필드를 제거하려고 할 때, CI 작업은 현재 운영 환경에 배포된 계약과 비교하여 차이점(diff)을 실행합니다. 만약 이 차이점이 필수 필드 제거와 같은 파괴적인 변경(breaking change)을 나타내면, 빌드가 실패합니다. 개발자는 심지어 배포 버튼을 누르기 전에 제지됩니다. 그들은 변화를 컨슈머(consumer)와 협상하거나 토픽 버전을 transactions_v2로 지정하도록 강제됩니다.
CI에서 게이트키퍼 구축하기
마법은 .github/workflows/contract-check.yaml 파일에 있습니다. 이 작업을 terraform plan이나 pytest 스위트와 동일한 심각도로 취급해야 합니다. 계약 검사가 실패하면, PR은 병합(merge)될 수 없습니다.
jobs:
validate-contract:
runs-on: ubuntu-latest
...
--compatibility backward 설정을 통해, 우리는 모든 새로운 배포가 엄격하게 가산적 (additive) 임을 보장합니다. 만약 개발자가 실수로 타입을 변경하면, CI 파이프라인 (CI pipeline)은 종료 코드(exit code) 1을 반환합니다. 이것은 단순한 린터 (linter)가 아닙니다. 이것은 자동화된 협상 (automated negotiation)입니다.
핀테크 (fintech) 분야에서 우리는 PCI-DSS 준수 문제를 다루었습니다. 만약 프로듀서 (producer)가 실수로 개인정보 (PII)를 노출하는 필드 이름을 변경한다면, 필드 이름에 대한 정규 표현식 (regex patterns)을 포함한 우리의 계약 검사 (contract checks)가 해당 유출을 잡아낼 것입니다. 이러한 규칙들을 PR 레벨에서 강제함으로써, 우리는 보안과 데이터 품질을 사후 대응적인 "운영 (ops)" 문제에서 사전 예방적인 "개발 (dev)" 문제로 전환했습니다.
사진 제공: Unsplash의 Brecht Corbeel
반대 의견 (그리고 나의 답변)
"이것이 우리의 속도 (velocity)를 늦출 것이다"라는 말이 제가 듣는 가장 흔한 반대 의견입니다. 동료 엔지니어들은 서비스 (services)를 반복 개선하는 것만큼이나 데이터 구조 (data structures)도 빠르게 반복 개선할 수 있어야 한다고 주장합니다.
나의 답변: 신뢰성 (reliability) 없는 속도는 그저 절벽을 향해 빠르게 달려가는 것뿐입니다. 프로덕션 (production) 환경에서 컨슈머 (consumer)를 망가뜨린다면, 당신은 단순히 "빠르게 움직이는 것"이 아닙니다. 당신은 다운스트림 의존성 체인 (downstream dependency chain)을 수정하기 위해 사고 대응 워룸 (incident response war room)에서 3일을 보내게 됩니다. 그것은 당신의 속도에 가해지는 엄청난 세금입니다. CI 체크를 위한 5분의 대기 시간은 속도를 늦추는 과속 방지턱이 아니라 투자입니다.
"파괴적 변경(breaking change)을 해야 하면 어떻게 하나요?"라고 그들은 묻습니다. 그럴 때는 스트림(stream)의 버전을 관리하면 됩니다. 만약 데이터의 의미를 변경하고 있다면, 당신은 근본적으로 새로운 제품을 만들고 있는 것입니다. 필드 타입(field type)을 변경한다면, 그것은 계약(contract)을 위반하는 것입니다. 새로운 토픽(topic)을 생성하고, 새로운 스키마(schema)를 게시한 뒤, 마이그레이션 기간 동안 두 가지를 병렬로 실행하세요. 네, 더 많은 작업이 필요합니다. 그것은 데이터가 일시적인 캐시(cache)가 아니라 영구적인 자산(asset)이기 때문입니다. 데이터의 생명주기(lifecycle)를 관리할 의지가 없다면, 다른 팀에 데이터를 노출해서는 안 됩니다.
"소규모 팀에게는 오버헤드(overhead)가 너무 큽니다." 만약 당신이 2명으로 구성된 팀이라면, 이런 것이 필요 없다고 생각할 수도 있습니다. 하지만 틀렸습니다. 당신은 단지 규모가 더 작을 뿐이며, 이는 치명적인 데이터 장애의 비용이 당신의 스타트업을 끝장낼 가능성이 더 높다는 것을 의미합니다. 계약 체크(contract check)를 조기에 자동화하세요. 서비스가 3개일 때는 설정하는 데 비용이 거의 들지 않습니다. 하지만 서비스가 300개가 되었을 때는 사후에 적용하는 것이 거의 불가능합니다.
결론
우리는 데이터 프로듀서(data producer)를 자신이 원할 때마다 세상의 형태를 바꿀 수 있는 왕처럼 대우하는 것을 멈춰야 합니다. 데이터는 우리가 관리하는 모든 금융 장부와 의료 기록의 토대입니다. 데이터를 부차적인 것으로 취급하는 것은 태만입니다.
데이터 계약(data contracts)을 리포지토리(repository)로 옮기고 CI에서 엄격한 실패(hard failures)로 강제함으로써, 당신은 마찰(friction)을 원래 있어야 할 곳으로 이동시킵니다. 즉, 변경 사항이 프로덕션(production)에 도달하는 순간이 아니라, 변경이 제안되는 순간으로 말입니다. 화려한 데이터 메시(data mesh)나 수백만 달러짜리 거버넌스(governance) 플랫폼은 필요하지 않습니다. 당신에게 필요한 것은 YAML 파일 하나, 기본적인 CLI 도구, 그리고 잘못된 코드를 차단할 수 있는 CI 파이프라인(CI pipeline)을 구축할 배짱입니다.
레지스트리(registry)를 신뢰하는 것을 멈추세요. 빌드를 깨뜨리기(breaking the build) 시작하세요. 당신의 다운스트림 컨슈머(downstream consumer)들이 당신에게 감사할 것입니다. 적어도, 새벽 3시에 당신에게 전화를 걸지는 않을 테니까요.
Cover photo by Fons Heijnsbroek on Unsplash.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기