침묵하는 데이터 손실은 내보내기 실패보다 더 위험하다
요약
데이터 내보내기 파이프라인에서 발생하는 '침묵하는 데이터 손실'의 위험성을 다룹니다. 유효한 데이터가 null이나 잘못된 불리언 값으로 처리되어 누락되는 버그를 수정하고, 데이터의 상태를 명확히 구분하는 설계의 중요성을 강조합니다.
핵심 포인트
- 내보내기 실패보다 데이터가 조용히 누락되는 것이 더 위험함
- 유효한 답변(Other, Unknown)을 null로 처리하지 않도록 주의
- 명시적 false와 '답변하지 않음'을 구분하는 3상태 값(tri-state) 도입
- 버그 재발 방지를 위한 최소한의 핵심 단위 테스트 작성
이번 주에 저는 모바일 내보내기 파이프라인(mobile export pipeline)에서 발생한 일련의 버그들을 수정했습니다. 무서운 점은 내보내기가 실패할 수 있다는 것이 아니었습니다.
데이터를 조용히 누락시킨 채로 내보내기가 성공할 수 있다는 점이었습니다.
이 파이프라인은 내부 평가 모델(internal assessment model)을 구축하고, 이를 검증하며, RdSAP XML 내보내기 형식으로 직렬화(serialize)하고, 증거를 패키징하며, 체크섬(checksum)을 작성하고, 감사 로그(audit log)를 기록합니다. 결정론적(deterministic)인 내보내기 작업은 이미 구현되어 있었습니다. 동일한 평가가 입력되면 동일한 바이트가 출력되는 방식입니다.
하지만 결정론적인 출력은 직렬화하려는 모델이 이미 의미를 잃지 않았을 때만 도움이 됩니다.
코드 리뷰를 통해 유효한 설문 답변이 "내보낼 내용 없음"으로 축소되는 몇몇 지점을 발견했습니다.
위험한 null
첫 번째 버그는 토큰 매퍼(token mapper)였습니다:
fuelToken(value): string | null
Other라는 유효한 답변이 null을 반환하고 있었습니다. 이는 주 연료(main fuel)나 온수 연료(water heating fuel)와 같은 필수 필드가 기존의 폴백 토큰(fallback token)으로 내보내지는 대신 XML에서 사라질 수 있음을 의미했습니다.
수정 방법은 지루했습니다:
Other -> "other"
지루한 것은 좋습니다. 지루하다는 것은 필드가 존재하며 다운스트림 검증(downstream validation)이 제 역할을 수행할 수 있음을 의미합니다.
동일한 버그의 두 번째 버전은 외풍 차단(draught proofing) 관련 문제였습니다. Unknown은 설문 UI에서 유효한 답변이었지만, 이를 나타낼 깔끔한 수치형 RdSAP 값이 없었습니다. 이전 동작은 이 값을 조용히 누락시켰습니다. 수정된 동작은 대신 차단되지 않는(non-blocking) 검증 경고를 발생시킵니다.
이 차이는 매우 중요합니다:
- 평가자가 답변하지 않아 누락된 경우: 생략(omit)
- 존재하지만 이 내보내기 형식으로 표현할 수 없는 경우: 경고(warn)
- 존재하며 표현 가능한 경우: 내보내기(export)
이것들은 세 가지 서로 다른 상태입니다. 이들을 모두 null로 취급하는 것이 바로 데이터 손실이 정중한 API를 갖게 되는 방식입니다.
false는 "답변하지 않음"이 아니다
더 미묘한 버그는 불리언(boolean) 처리였습니다.
기존의 헬퍼(helper) 함수는 명시적인 false와 "답변하지 않음"을 사실상 동일한 출력으로 축소했습니다. 따라서 실린더 존재 여부(cylinder present), 침수 히터(immersion heater) 또는 단열(insulation)과 같은 필드에 대해 기록된 "아니오(no)\
그것은 잘못된 거래입니다. 평가/내보내기 (assessment/export) 시스템에서 "아니오(no)"는 데이터입니다.
이제 헬퍼(helper)는 다음과 같은 3상태 값 (tri-state value)을 반환합니다:
boolean | null
그리고 시리얼라이저 (serializer)는 실제 답변에 대해서는 true/false를 출력하며, 값이 진정으로 답변되지 않았을 때만 해당 요소를 생략합니다.
테스트는 매우 작으며, 이것이 바로 핵심입니다:
it("명시적인 false 답변과 답변되지 않은 상태를 구분한다", () => {
const a = buildAssessment({
inspection,
...
거대한 테스트 하네스 (test harness)도 필요 없습니다. 그저 버그가 다시 발생하는 것을 방지하는 단 하나의 단언 (assertion)만 있으면 됩니다.
원시 JSON (raw JSON)에서 추측하지 마세요
지나치게 과하게 작동하려 했던 LiDAR 바닥 면적 파서 (floor-area parser)도 있었습니다.
JSON에 total이 있다면 그것을 사용합니다. 좋습니다.
만약 total이 누락되었다면, 기존의 폴백 (fallback) 방식은 원시 JSON을 스캔하여 숫자를 찾아냈습니다. 이는 실수로 개별 방의 면적을 가져와 해당 부동산의 전체 바닥 면적으로 취급할 위험이 있었습니다.
이는 실패하는 것보다 더 나쁩니다.
수정된 버전은 total이 누락되었을 때 null을 반환합니다. 영웅적인 추측은 하지 않습니다. "데이터 덩어리(blob) 어딘가에서 20을 찾았으니 아마도 20m²일 것이다"와 같은 방식은 없습니다.
expect(a.property.floorAreaM2).toBeNull();
다시 강조하지만, 지루한 테스트가 유용한 보증을 제공합니다.
유출되기 전에 감사 실패를 점검하세요
또 다른 버그는 시리얼라이제이션 (serialization) 외부에 있었습니다. getExporter()가 메인 try/catch 이전에 예외를 던질 수 있었는데, 이는 포맷 조회 실패 시 다른 실패들이 기록하는 감사 추적 (audit-trail) 쓰기를 건너뛰게 된다는 것을 의미했습니다.
이것이 바로 운영 환경의 디버깅을 짜증 나게 만드는 에지 케이스 (edge case) 유형입니다. 내보내기는 실패했지만, 실패 경로에서 증거를 남기는 것을 잊어버린 것입니다.
해결책은 단순히 조회 로직을 보호된 경로 (guarded path) 내부로 이동시켜 감사 로그 (audit log)가 일관되게 작성되도록 하는 것이었습니다.
AI 보조 부분
이 부분이 저에게 AI 보조 개발 (AI-assisted development)이 실제로 유용한 지점입니다.
"내보내기 로직 전체를 작성하고 운에 맡기는 것"이 아닙니다. 오히려 다음과 같습니다:
- 결정론적인 이음매 (deterministic seam)를 구축한다.
- Claude에게 지루한 매핑 로직 (mapping logic)을 검토하게 한다.
- 발견된 모든 모호함을 좁은 범위의 테스트로 전환한다.
- 의도적인 유보 (deliberate deferrals)를 명시적으로 유지한다.
이번 패스(pass)를 통해 6개의 집중 테스트(focused tests)가 추가되었습니다. 내보내기 스위트(export suite)는 119개에서 125개로 늘어났으며, 대상 내보내기 테스트(targeted export tests)는 로컬에서 통과되었습니다: 3개 스위트, 23개 테스트.
여전히 해결되지 않은 채 남겨둔 알려진 더 큰 이슈가 하나 있습니다: 현재 검증(validation) 과정이 모든 내보내기 형식(export format)에 대해 너무 RdSAP에 특화되어 있다는 점입니다. 이는 형식별 검증 프로필(per-format validation profiles)이 필요합니다. 이것은 별개의 변경 사항이기 때문에 이번 수정 사항에 억지로 끼워 넣지는 않았습니다.
이것은 실제 코드베이스(codebases)에서 AI와 함께 작업할 때 적용할 수 있는 또 다른 유용한 규칙입니다: 지금 당장의 침묵하는 데이터 손실(silent data loss)은 수정하되, 더 큰 설계 변경 사항은 눈에 보이도록 남겨두고, 모델이 버그 수정(bug fix)을 재작성(rewrite)으로 변질시키지 않도록 하는 것입니다.
최고의 내보내기 파이프라인(export pipeline)은 가장 영리한 것이 아닙니다.
조용히 거짓말하기를 거부하는 파이프라인입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기