순수 파이썬으로 데이터 계약 검증기를 만들고 매출 30% 손실을 포착했습니다.
Source: Dev.to
몇 달 전, 하루의 대부분을 버그를 잡는 데 보냈습니다. 그 버그는 사실 버그가 아니었습니다. 하위 대시보드에 매출이 하룻밤 사이에 30% 급등했다고 표시되었습니다. 배포도 없고, 스키마 변경도 없으며, 로그에도 아무것도 없었습니다. 너무 오래 찾은 끝에 발견했습니다: 상위 시스템이 total 컬럼을 보내기 시작했는데, 이 값이 더 이상 subtotal + tax와 일치하지 않았던 것입니다. 파이프라인은 충돌하지 않았고, 데이터는 조용히 거짓말을 했으며, 모든 하위 시스템이 이를 믿었습니다.
데이터 버그는 그런 것이죠. 예외를 거의 발생시키지 않습니다. 상태 필드에 새로운 오타값이 생기고, 조인 키가 고아 레코드를 만들고, “실제로는 절대 null이 되지 않던” nullable 컬럼이 갑자기 null이 됩니다. 이 모든 것이 시스템을 중단시키지는 않지만, 의사결정에 쓰이는 숫자를 부패시킵니다.
그래서 저는 DataPact를 만들었습니다: 데이터가 어떠해야 하는지를 기록하고, 이를 강제하는 작은 프레임워크입니다. 데이터 품질 및 데이터 계약 검증 도구이며, 전부 Python 표준 라이브러리만으로 동작합니다. pandas도, PyYAML도, 네트워크 호출도 없습니다.
실시간 데모 보고서: https://hajirufai.github.io/datapact/report.html
랜딩 페이지: https://hajirufai.github.io/datapact/
소스: https://github.com/hajirufai/datapact
아이디어: 여기저기 흩어진 어설션이 아닌 계약
대부분의 팀은 이미 데이터를 검증합니다—하지만 보통은 노트북이나 DAG에 파묻힌 임시 assert df["x"].notna().all() 같은 코드 조각들의 더미에 불과합니다. “orders 테이블에 대한 규칙이 뭐야?” 라는 질문에 세 개의 레포를 grep하지 않고는 답할 수 없습니다.
데이터 계약은 이 흐름을 뒤집습니다. 컬럼 타입, null 규칙, 범위, 허용 집합, 정규식, 교차 컬럼 연산, 참조 무결성 등을 선언적인 하나의 문서에 적고, Git으로 버전 관리한 뒤, 생산자와 소비자가 이를 공유합니다. DataPact는 그 계약에 따라 배치를 검증하고, 정확히 어떤 부분이 깨졌는지 알려줍니다.
아래는 DataPact의 YAML‑lite 형식 계약 예시입니다:
name: orders
version: 1.0
strictness: lenient
columns:
- name: order_id
type: int
nullable: false
checks:
- kind: column_values_unique
severity: error
- name: status
type: str
checks:
- kind: column_values_in_set
kwargs: { values: [new, paid, shipped, refunded] }
expectations:
- kind: multicolumn_sum_to_equal
kwargs: { columns: [subtotal, tax], total_column: total, tolerance: 0.01 }
Enter fullscreen mode
Exit fullscreen mode
마지막 expectations 항목이 바로 제가 겪은 30% 매출 급증을 잡아낼 수 있었던 정확한 규칙입니다. subtotal + tax는 total과 1센트 이내의 오차로 일치해야 합니다.
“Zero dependencies”는 허세가 아니다
왜 표준 라이브러리만 사용했는지 솔직히 말씀드리자면, 이것이 자랑거리처럼 들릴 수도 있지만 실제로는 두 가지 실질적인 이유가 있습니다.
-
많은 데이터 플랫폼이 보안상 제한됩니다. 파이프라인이 실행되는 환경에 PyPI 패키지를
pip install할 수 없는 경우가 많습니다. pandas + pyarrow + YAML 파서를 끌고 다니는 검증 도구보다, Python만 있으면 바로 사용할 수 있는 도구가 채택하기 훨씬 쉽습니다. -
라이브러리를 얕게 감싸는 것이 아니라 문제 자체를 이해하고 싶었습니다. 직접 YAML 파서와 타입 추론 로직을 구현하면서 “이 컬럼은 어떤 타입이어야 하는가”라는 복잡한 현실을 배울 수 있었고, 어떤 래퍼도 제공하지 못하는 통찰을 얻었습니다.
그 대가로 YAML 파서를 직접 구현해야 했고, 여기서 프로젝트 전체를 가장 골치 아프게 만든 버그가 등장했습니다.
모든 이메일을 깨뜨린 이스케이프 시퀀스 버그
DataPact는 자체적인 작은 YAML 리더를 제공합니다—지원하는 부분은 맵, 리스트, 스칼라, 주석, 따옴표, 흐름 리스트 등 제한된 서브셋입니다. 임의 객체 역직렬화를 지원하지 않기 때문에 보안상 좋은 속성을 자연스럽게 얻을 수 있습니다.
계약에 넣은 이메일 검증 정규식은 다음과 같았습니다:
checks:
- kind: column_values_match_regex
kwargs:
pattern: "^[^@ ]+@[^@ ]+\\.[^@ ]+$"
Enter fullscreen mode
Exit fullscreen mode
실행했을 때 모든 이메일이 실패했습니다. 당연히 유효한 이메일조차도 14개 중 14개, 100%가 실패했죠. 처음 만든 파서는 주변 따옴표만 제거하고 원시 문자열을 그대로 반환했기 때문에 \\.가 그대로 “백슬래시‑백슬래시‑점”으로 남았습니다. 컴파일된 정규식에서는 “백슬래시 뒤에 어떤 문자든 올 수 있다”는 의미가 되며, 실제 이메일에 백슬래시가 들어가는 경우는 없으니 모두 불일치가 된 것이었습니다.
수정 방법은 파서를 실제 YAML이 하는 대로 동작하게 만드는 것이었습니다: double‑quoted 문자열 안의 이스케이프 시퀀스를 처리하고, single‑quoted 문자열은 그대로 반환하도록 말이죠.
_ESCAPES = {"n": "\n", "t": "\t", "r": "\r", '"': '"', "\\": "\\", "/": "/", "0": "\0"}
def _unescape_double(s: str) -> str:
out, i = [], 0
while i B[Dataset
+ type inference]
C[Contract
YAML / JSON / builder] --> D[Validation Engine]
B --> D
D --> E[ValidationReport]
E --> F[CLI exit code]
E --> G[HTML report]
E --> H[guard / raise]
B --> P[Profiler] --> C
Enter fullscreen mode
Exit fullscreen mode
Sources는 CSV, JSON, JSONL, SQLite, 그리고 일반적인 리스트‑오브‑딕셔너리를 하나의 Dataset 뷰로 정규화합니다.
Expectations는 순수 함수 레지스트리이며, 체크 종류당 하나씩 존재합니다. 컬럼 수준(not_null, unique, between, in_set, match_regex, mean_between…), 테이블 수준(row_count_between, compound_columns_unique…), 교차 컬럼(a > b, sum_to_equal, referential integrity`) 총 23개의 체크가 제공됩니다.
Engine은 모든 expectation을 실행하고, 예상치 못한 컬럼에 대한 strictness 규칙을 적용한 뒤, 구조화된 ValidationReport를 생성합니다.
각 컬럼 수준 체크는 mostly= 허용 오차를 지원합니다. 즉 “전체 행의 99% 이상은 null이 아니어야 한다”와 같이 완벽함을 강요하지 않고도 검증할 수 있습니다—실제 데이터는 지저분하고, 하나의 나쁜 행 때문에 1천만 행 배치를 전체 실패로 만들고 싶지는 않으니까요.
사용 방법: 세 가지 방식
1️⃣ 라이브러리로 사용 – 계약 파일에 대해 검증
import datapact as dp
report = dp.validate("orders.csv", dp.load_contract("orders.yaml"))
print(report.success, report.passed, report.failed)
for r in report.results:
if not r.success:
print(r.expectation.label(), "→", r.message)
Enter fullscreen mode
Exit fullscreen mode
2️⃣ 파이프라인 게이트로 사용 – 데코레이터로 나쁜 데이터 차단
from datapact import guard, DataContractError
@guard(contract)
def load_orders():
return fetch_rows_from_somewhere()
try:
rows = load_orders()
except DataContractError as exc:
alert(exc.report) # 전체 보고서가 예외에 첨부됩니다
Enter fullscreen mode
Exit fullscreen mode
3️⃣ CI 체크로 사용 – 계약 위반 시 빌드 실패
datapact validate orders.csv --contract orders.yaml --fail-on error
echo $? # 위반 시 1 반환
Enter fullscreen mode
Exit fullscreen mode
--fail-on 플래그가 제가 가장 만족하는 부분입니다. 데이터 품질을 대시보드가 아니라 게이트로 만들 수 있기 때문이죠. 제 GitHub Actions 워크플로우는 두 개의 잡을 실행합니다: 하나는 깨끗한 샘플이 통과함을 증명하고, 다른 하나는 오염된 샘플이 실패