A Validator That Cannot Say "Passed" Is Not a Validator
한국어 원문으로 표시합니다.
한 줄 요약
검증기의 가치는 무엇을 잡아내느냐가 아니라 정상 파일에 대해 정상이라고 말할 수 있느냐에서 결정된다.
왜 이게 필요했나
정제 스크립트는 한 번 쓰고 버립니다. 검증기는 매주 돕니다. 이 차이가 설계를 바꿉니다.
고객사에 데이터 연동을 붙이고 나면, 다음 달부터 매주 같은 형식의 파일이 도착합니다. 그중 어느 주에는 상류 시스템이 바뀌어서 열이 하나 늘거나, 날짜 형식이 달라지거나, 인코딩이 바뀝니다. 그 변화를 사람이 눈으로 발견하는 시점은 대개 집계 숫자가 이상하다는 신고가 들어온 뒤입니다.
검증기는 그 시차를 없애기 위한 것입니다.
어떻게 동작하나
쓸 만한 검증기는 네 종류의 규칙을 가집니다.
스키마 규칙 — 열의 개수와 이름, 필수 값의 존재, 타입. "필드가 5개여야 하고, customer 는 비어 있으면 안 되고, amount 는 정수여야 한다."
중복 규칙 — 키가 유일한가. 여기서 주의할 것은 앞 모듈에서 본 함정입니다. 키에 결측이 있을 수 있으면 결측 행은 별도로 셉니다.
범위 규칙 — 값이 상식적인 구간에 있는가. amount 가 0 이거나 음수이거나 9,900만 원인 행은 형식상 유효한 정수지만 업무상으로는 거의 확실히 잘못된 데이터입니다. 범위 규칙이 없는 검증기는 타입만 맞으면 전부 통과시켜서, 소수점 위치가 하나 밀린 값을 그대로 흘려보냅니다.
참조 규칙 — 다른 데이터와의 정합성. 주문의 고객 번호가 고객 테이블에 실제로 있는가.
그리고 이 네 가지보다 중요한 성질이 하나 있습니다. 정상 파일에 대해 반드시 통과라고 말해야 합니다.
현장에서 만나는 모습
검증기가 실패하는 두 가지 방식이 있고, 둘 다 흔합니다.
과탐지. 규칙이 너무 빡빡해서 정상 파일도 실패합니다. 그러면 사람이 매주 "또 그거겠지" 하며 무시하기 시작하고, 두 달 뒤 진짜 문제가 왔을 때도 똑같이 무시합니다. 그 시점에는 검증기가 있는 것이 없는 것보다 나쁩니다. 있다고 믿게 만들면서 실제로는 아무것도 막지 못하기 때문입니다.
과소탐지. 규칙이 느슨해서 아무것도 못 잡습니다. 대개 "일단 죽지 않게" 만든 검증기가 이렇게 됩니다.
그래서 검증기를 만들면 반드시 두 방향으로 시험합니다. 정상 파일에 돌려 통과하는지, 깨진 파일에 돌려 실패하는지. 둘 중 하나만 확인한 검증기는 절반만 만든 것입니다.
마지막으로 출력 형식입니다. 검증기는 종료 코드로 말해야 합니다. 통과면 0, 실패면 0 이 아닌 값. 그래야 CI 나 배치 파이프라인에 그대로 꽂힙니다. 사람이 읽을 메시지는 그 위에 얹는 것이고, 기계가 읽을 신호가 먼저입니다.
검증기가 갖춰야 할 것
"틀렸다" 만 말하는 검증기는 반쪽입니다. 사람이 고칠 수 있으려면 네 가지가 필요합니다.
| 요소 | 없으면 |
|---|---|
| 어느 줄·어느 필드인가 | 100만 행 중 어디인지 찾느라 하루 |
| 무엇을 기대했나 | 무엇으로 고쳐야 할지 모른다 |
| 실제 값은 무엇인가 | 원본을 다시 열어 봐야 한다 |
| 몇 건인가 | 하나짜리 예외인지 구조적 문제인지 모른다 |
❌ 검증 실패: 잘못된 데이터가 있습니다
✅ 행 4213, 컬럼 order_date: '2026-13-45' 가 날짜 형식이 아닙니다 (기대: YYYY-MM-DD)
같은 오류 1,842건 — 원본의 8행 이후 전부. 인코딩이나 구분자를 먼저 확인하세요.
마지막 문장이 핵심입니다. 오류가 특정 지점 이후로 몰려 있으면 개별 값의 문제가 아니라 파싱 문제 입니다. 검증기가 그 패턴을 알아채고 알려 주면 조사 방향이 바로 잡힙니다.
어디서 막을 것인가
같은 검사를 여러 층에 두는 것은 낭비가 아닙니다. 각각 다른 것을 막습니다.
1. 수집 경계 형식·필수 필드·인코딩 → 나쁜 데이터가 들어오지 못하게
2. 변환 중 비즈니스 규칙·참조 무결성 → 조용히 잘못된 결과를 만들지 못하게
3. 적재 후 행 수·합계·분포 대조 → 옮기다 잃어버린 것을 잡게
3번을 자주 빠뜨립니다. 원본 12,043행이 적재 후 12,041행이면 2행이 어디선가 사라진 것입니다. 행 수와 금액 합계를 대조하는 한 줄 이 이런 것을 잡습니다.
-- 적재 후 대조
select
(select count(*) from staging_orders) as src,
(select count(*) from orders where load_id = :id) as dst,
(select sum(amount) from staging_orders) as src_amt,
(select sum(amount) from orders where load_id = :id) as dst_amt;
나쁜 행을 어떻게 다루나
전부 실패시키면 좋은 데이터까지 못 씁니다. 하나도 안 막으면 오염됩니다. 실무의 답은 격리 입니다.
읽은 행 12,043
├ 통과 11,998 → 적재
└ 격리 45 → quarantine 표 + 사유
격리된 행은 사유와 함께 남기고, 격리 건수에 임계를 겁니다. 평소 0.1% 이던 것이 5% 로 뛰면 원본 쪽에 무언가 바뀐 것이므로 그 자체가 경보입니다. 조용히 버리면 이 신호가 사라집니다.
다음 실습에서 할 것
의도적으로 서로 다른 방식으로 깨진 파일 세 개와 멀쩡한 파일 하나를 놓고, 각각 몇 행이 규칙을 어기는지 세고, 마지막에 통과와 실패를 둘 다 판정할 수 있는 검증 스크립트를 만듭니다.