<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Data Quality on jyukki's Blog</title><link>https://jyukki.com/tags/data-quality/</link><description>Recent content in Data Quality on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-kr</language><lastBuildDate>Thu, 09 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/data-quality/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: Domain Invariant Registry, 데이터 품질을 운영 계약으로 관리하기</title><link>https://jyukki.com/learning/deep-dive/deep-dive-domain-invariant-registry-data-quality-playbook/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-domain-invariant-registry-data-quality-playbook/</guid><description>주문·결제·권한·재고처럼 틀리면 복구 비용이 큰 데이터를 사후 보정이 아니라 도메인 불변식, 검증 지점, 위반 SLO, 보정 경로로 관리하는 실무 플레이북입니다.</description><content:encoded><![CDATA[<p>서비스가 커질수록 데이터 품질 문제는 &ldquo;입력값 검증을 더 잘하자&quot;만으로 해결되지 않습니다. 주문은 결제와 배송, 쿠폰, 포인트, 정산, CS 화면으로 퍼지고, 권한은 사용자·조직·역할·정책 캐시·검색 인덱스로 복사됩니다. 어느 한 지점에서만 정상이어도 전체 업무 규칙은 깨질 수 있습니다. <code>NOT NULL</code>과 enum은 필요하지만, &ldquo;환불된 주문은 배송 요청을 만들 수 없다&quot;나 &ldquo;관리자 권한은 tenant 경계를 넘을 수 없다&rdquo; 같은 규칙은 더 높은 수준의 도메인 불변식입니다.</p>
<p>Domain Invariant Registry는 이런 중요한 규칙을 머릿속 지식이 아니라 운영 계약으로 등록하는 방식입니다. 각 불변식에 owner, 심각도, 보장 위치, 탐지 쿼리, 허용 지연, 보정 경로, 종료 기준을 붙입니다. 이 글은 <a href="/learning/deep-dive/deep-dive-operational-state-machine-design/">운영용 상태 머신 설계</a>, <a href="/learning/deep-dive/deep-dive-snapshot-isolation-serializable-write-skew-playbook/">Snapshot Isolation과 Write Skew</a>, <a href="/learning/deep-dive/deep-dive-reconciliation-ledger-pipeline/">Reconciliation Ledger Pipeline</a>, <a href="/learning/deep-dive/deep-dive-correction-job-audit-guardrails-playbook/">Correction Job 플레이북</a>을 데이터 품질 운영 관점으로 연결합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>도메인 불변식과 일반 입력 검증, DB 제약, 배치 검증의 차이를 구분할 수 있습니다.</li>
<li>불변식을 P0/P1/P2로 나누고, 어떤 규칙은 쓰기 경로에서 막고 어떤 규칙은 reconciliation으로 잡을지 판단할 수 있습니다.</li>
<li>데이터 품질 알람을 단순 &ldquo;불일치 몇 건&quot;이 아니라 고객 영향, 허용 지연, 보정 가능성 기준으로 설계할 수 있습니다.</li>
<li>운영팀과 개발팀이 같은 언어로 데이터 품질을 설명할 수 있는 registry 템플릿을 가져갈 수 있습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-불변식은-항상-참이어야-하는-업무-규칙이다">1) 불변식은 &ldquo;항상 참이어야 하는 업무 규칙&quot;이다</h3>
<p>입력 검증은 보통 요청 하나를 봅니다. <code>price &gt;= 0</code>, <code>email format</code>, <code>required field</code> 같은 규칙입니다. DB 제약은 row나 table 수준을 잘 닫습니다. <code>UNIQUE(tenant_id, sku)</code>, <code>NOT NULL</code>, <code>CHECK(amount &gt;= 0)</code> 같은 형태입니다. 하지만 운영 사고는 자주 여러 row, 여러 시스템, 여러 시간대를 건드립니다.</p>
<p>예를 들면 아래 규칙은 단순 입력 검증보다 큽니다.</p>
<table>
  <thead>
      <tr>
          <th>도메인</th>
          <th>불변식 예시</th>
          <th>깨졌을 때 영향</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>결제</td>
          <td>결제 승인 합계는 주문 청구 금액을 초과할 수 없다</td>
          <td>중복 청구, 정산 오류</td>
      </tr>
      <tr>
          <td>권한</td>
          <td>사용자는 자신이 속한 tenant 밖의 리소스를 볼 수 없다</td>
          <td>보안 사고</td>
      </tr>
      <tr>
          <td>재고</td>
          <td>확정 주문 수량과 예약 수량의 합은 판매 가능 수량을 넘을 수 없다</td>
          <td>초과 판매</td>
      </tr>
      <tr>
          <td>개인정보</td>
          <td>삭제 요청이 완료된 식별자는 검색·분석 인덱스에 남지 않아야 한다</td>
          <td>규정 위반</td>
      </tr>
      <tr>
          <td>상태</td>
          <td><code>CANCELED</code> 주문은 새 배송 작업을 만들 수 없다</td>
          <td>잘못된 fulfillment</td>
      </tr>
  </tbody>
</table>
<p>이 규칙은 &ldquo;어딘가 코드에 있다&quot;만으로 충분하지 않습니다. 어느 코드에 있는지, DB가 막는지, 캐시나 검색 인덱스에는 언제 반영되는지, 이미 깨졌는지 어떻게 아는지까지 운영 계약으로 가져와야 합니다.</p>
<h3 id="2-모든-불변식을-쓰기-경로에서-100-막을-수는-없다">2) 모든 불변식을 쓰기 경로에서 100% 막을 수는 없다</h3>
<p>가장 안전한 방식은 쓰기 경로에서 깨지는 데이터를 만들지 않는 것입니다. 결제 중복 승인, 권한 상승, 잔액 음수처럼 복구 비용이 큰 규칙은 가능한 한 transaction, unique constraint, 조건부 update, lock row, serializable transaction으로 닫아야 합니다.</p>
<p>하지만 모든 규칙을 같은 방식으로 막을 수는 없습니다. 검색 인덱스 삭제 전파, read model lag, 외부 배송사 상태 동기화처럼 비동기 복사본이 얽힌 규칙은 짧은 지연이 정상입니다. 이때 중요한 것은 &ldquo;언젠가 맞겠지&quot;가 아니라 허용 지연을 숫자로 정하는 것입니다.</p>
<p>초기 기준은 아래처럼 둘 수 있습니다.</p>
<ul>
<li>P0 불변식: 쓰기 경로 fail-closed, 위반 감지 1건이면 Sev 분류</li>
<li>P1 불변식: 허용 지연 p95 30초~5분, residual count 임계치 초과 시 알람</li>
<li>P2 불변식: 일 1회 이상 검증, 추세 악화 시 backlog ticket</li>
<li>고위험 도메인: 금전·권한·개인정보·재고는 &ldquo;위반 건수 0&quot;을 기본 목표로 둠</li>
<li>파생 데이터: 검색·추천·통계는 freshness SLO와 원본 재확인 경로를 함께 둠</li>
</ul>
<h3 id="3-registry는-문서가-아니라-실행-가능한-색인이다">3) Registry는 문서가 아니라 실행 가능한 색인이다</h3>
<p>불변식 목록을 위키에만 쓰면 금방 오래됩니다. Registry는 최소한 detection query, metric, owner, correction path와 연결되어야 합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ff79c6">domain_invariant</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">id</span>: INV-PAYMENT-001
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">name</span>: <span style="color:#f1fa8c">&#34;captured_amount_must_not_exceed_order_total&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">owner</span>: payments-platform
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">severity</span>: P0
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">source_of_truth</span>: payment_ledger
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">guarantee_layer</span>:
</span></span><span style="display:flex;"><span>    - db_unique_constraint
</span></span><span style="display:flex;"><span>    - conditional_update
</span></span><span style="display:flex;"><span>    - reconciliation_query
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">detection</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">schedule</span>: <span style="color:#f1fa8c">&#34;*/5 * * * *&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">expected_violation_count</span>: <span style="color:#bd93f9">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">alert_after</span>: <span style="color:#f1fa8c">&#34;1 violation&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">correction</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">path</span>: <span style="color:#f1fa8c">&#34;manual_approval_correction_job&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">required_evidence</span>:
</span></span><span style="display:flex;"><span>      - before_after_ledger_sample
</span></span><span style="display:flex;"><span>      - amount_delta_sum
</span></span><span style="display:flex;"><span>      - affected_user_count
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">closure</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">residual_count</span>: <span style="color:#bd93f9">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">reconciliation_rerun</span>: passed
</span></span></code></pre></div><p>이 정도 필드가 있으면 장애 때 질문이 달라집니다. &ldquo;결제 데이터 이상한 것 같아요&quot;가 아니라 &ldquo;INV-PAYMENT-001 위반 3건, 총 금액 42,000원, correction job 승인 필요&quot;처럼 말할 수 있습니다.</p>
<h3 id="4-불변식-위반은-원인보다-먼저-영향도를-분류한다">4) 불변식 위반은 원인보다 먼저 영향도를 분류한다</h3>
<p>운영 중 불변식 위반이 나오면 개발자는 원인을 찾고 싶어 합니다. 물론 원인 분석은 필요합니다. 하지만 먼저 할 일은 영향도 분류입니다. 고객에게 노출됐는지, 자동 보정 가능한지, 쓰기 경로를 잠시 닫아야 하는지 판단해야 합니다.</p>
<p>분류 기준은 아래 순서가 현실적입니다.</p>
<ol>
<li>고객 데이터, 금전, 권한, 삭제/비공개 노출 여부</li>
<li>현재도 위반이 증가 중인지, 과거 잔여분인지</li>
<li>source of truth가 명확한지</li>
<li>자동 보정 가능한지, 승인 보정이 필요한지</li>
<li>재발 방지를 위해 쓰기 경로를 즉시 차단해야 하는지</li>
</ol>
<p>source of truth가 흔들리는 경우에는 보정이 아니라 동결이 먼저입니다. 어느 값이 맞는지 모르는 상태에서 correction job을 돌리면 원장과 감사 로그까지 오염될 수 있습니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-핵심-불변식-10개부터-시작한다">1) 핵심 불변식 10개부터 시작한다</h3>
<p>처음부터 모든 규칙을 등록하려 하면 실패합니다. 우선순위는 복구 비용 기준으로 잡습니다.</p>
<ul>
<li>결제·환불·포인트·정산: 금액 합계, 중복 효과, ledger balance</li>
<li>권한·tenant: tenant boundary, 관리자 권한, 정책 캐시 무효화</li>
<li>개인정보·삭제: 삭제 전파, 검색/분석 인덱스 잔존, 외부 전송</li>
<li>재고·예약: 예약 만료, 확정 수량, 초과 판매</li>
<li>공개 상태: 스캔 전 파일 공개, 비공개 콘텐츠 노출</li>
</ul>
<p>각 도메인에서 2개씩만 뽑아도 10개가 됩니다. 이 10개는 PR 템플릿, 테스트, reconciliation, 알람, correction job 중 적어도 하나와 연결합니다.</p>
<h3 id="2-보장-위치를-네-층으로-나눈다">2) 보장 위치를 네 층으로 나눈다</h3>
<p>불변식마다 어디서 보장할지 정합니다.</p>
<table>
  <thead>
      <tr>
          <th>보장 위치</th>
          <th>적합한 규칙</th>
          <th>예시</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB 제약</td>
          <td>단일 row, unique, 단순 합법 범위</td>
          <td>amount &gt;= 0, unique idempotency key</td>
      </tr>
      <tr>
          <td>트랜잭션/도메인 서비스</td>
          <td>상태 전이, 권한, 재고 차감</td>
          <td>PAID -&gt; CANCELED 조건부 update</td>
      </tr>
      <tr>
          <td>비동기 reconciliation</td>
          <td>파생 데이터, 외부 시스템 동기화</td>
          <td>검색 인덱스 삭제 전파, 배송 상태 비교</td>
      </tr>
      <tr>
          <td>correction job</td>
          <td>이미 깨진 데이터 보정</td>
          <td>누락 포인트 재계산, 잘못된 권한 회수</td>
      </tr>
  </tbody>
</table>
<p>P0는 가능하면 앞 두 층에서 막고, reconciliation은 누락 감시로 둡니다. P1은 비동기 검증과 correction path가 핵심입니다. P2는 일일 리포트로 충분한 경우가 많습니다.</p>
<h3 id="3-알람은-위반-건수보다-위험-조합으로-만든다">3) 알람은 위반 건수보다 위험 조합으로 만든다</h3>
<p>불변식 알람을 단순 카운터로 만들면 노이즈가 큽니다. 같은 10건이라도 금액 0원인 파생 통계와 관리자 권한 부여는 위험이 다릅니다.</p>
<p>권장 알람 기준:</p>
<ul>
<li>P0: 1건 발생 즉시 온콜, 15분 내 owner 판정</li>
<li>P1: residual count 10건 초과 또는 허용 지연 p95 5분 초과 시 알람</li>
<li>P2: 일일 리포트, 3일 연속 증가 시 backlog 우선순위 상승</li>
<li>source of truth unknown: 건수와 무관하게 수동 확인</li>
<li>같은 invariant가 7일 내 3회 이상 재발하면 재발 방지 ticket 필수</li>
</ul>
<p>알람 메시지에는 invariant id, owner, detection query version, 위반 샘플, 예상 correction path가 들어가야 합니다. 로그 링크만 던지면 운영자는 다시 추리를 시작해야 합니다.</p>
<h3 id="4-pr과-배포에-invariant-delta를-붙인다">4) PR과 배포에 invariant delta를 붙인다</h3>
<p>불변식은 운영 중에만 볼 문제가 아닙니다. 스키마 변경, 상태 전이 변경, 이벤트 소비자 변경, 캐시 정책 변경, 검색 인덱스 변경은 기존 invariant를 약하게 만들 수 있습니다.</p>
<p>고위험 PR에는 아래 질문을 붙입니다.</p>
<ul>
<li>새 불변식이 생기는가?</li>
<li>기존 불변식의 보장 위치가 바뀌는가?</li>
<li>source of truth가 바뀌는가?</li>
<li>비동기 지연 허용 시간이 바뀌는가?</li>
<li>위반 시 correction path가 있는가?</li>
</ul>
<p>이 질문은 <a href="/learning/deep-dive/deep-dive-consumer-driven-contract-testing/">Consumer-Driven Contract Testing</a>과 비슷합니다. API 계약을 깨면 consumer가 깨지듯, 도메인 불변식을 바꾸면 운영 데이터 품질이 깨집니다.</p>
<h3 id="5-운영-리포트는-위반-수보다-종료-가능성을-보여준다">5) 운영 리포트는 &ldquo;위반 수&quot;보다 종료 가능성을 보여준다</h3>
<p>불변식 리포트는 단순히 <code>violation_count=37</code>을 보여주면 부족합니다. 운영자가 알고 싶은 것은 &ldquo;지금 고객 영향이 있는가&rdquo;, &ldquo;어떤 원장을 기준으로 맞출 수 있는가&rdquo;, &ldquo;보정을 끝냈다고 말할 수 있는가&quot;입니다. 그래서 리포트는 위반 수, 영향 추정, source of truth, 보정 상태, 종료 조건을 같이 보여줘야 합니다.</p>
<p>예를 들어 권한 캐시 불일치 리포트는 아래처럼 나눌 수 있습니다.</p>
<table>
  <thead>
      <tr>
          <th>필드</th>
          <th>예시</th>
          <th>해석</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>invariant_id</td>
          <td>INV-AUTH-003</td>
          <td>tenant boundary 관련 규칙</td>
      </tr>
      <tr>
          <td>current_violations</td>
          <td>12</td>
          <td>지금 탐지된 불일치 건수</td>
      </tr>
      <tr>
          <td>customer_exposed</td>
          <td>0</td>
          <td>실제 노출 증거가 있는 건수</td>
      </tr>
      <tr>
          <td>source_of_truth</td>
          <td>auth_policy_ledger</td>
          <td>보정 기준이 되는 원장</td>
      </tr>
      <tr>
          <td>oldest_violation_age</td>
          <td>8m</td>
          <td>허용 지연 초과 여부 판단</td>
      </tr>
      <tr>
          <td>correction_state</td>
          <td>pending_approval</td>
          <td>자동 보정이 아니라 승인 대기</td>
      </tr>
      <tr>
          <td>close_condition</td>
          <td>residual_count=0 and rerun_passed</td>
          <td>종료 선언 기준</td>
      </tr>
  </tbody>
</table>
<p>이 형식이 있으면 팀은 &ldquo;몇 건이냐&quot;보다 &ldquo;어떤 경로로 닫을 수 있느냐&quot;를 먼저 봅니다. 특히 금전·권한·개인정보 규칙은 고객 노출 증거가 0이어도 source of truth가 불명확하면 보류가 맞습니다. 반대로 source of truth가 명확하고 residual count가 작으면 승인 보정으로 빠르게 닫을 수 있습니다.</p>
<h3 id="6-도입-순서는-incident-후보에서-역산한다">6) 도입 순서는 incident 후보에서 역산한다</h3>
<p>처음 registry를 만들 때는 도메인 모델을 위에서 아래로 훑는 것보다 최근 장애, CS, 수동 보정, 정산 차이, 권한 문의에서 역산하는 편이 빠릅니다. 이미 비용을 만든 사건은 좋은 후보입니다.</p>
<p>도입 순서 예시는 아래처럼 잡을 수 있습니다.</p>
<ol>
<li>최근 3개월의 데이터 보정 ticket과 장애 리포트를 모은다.</li>
<li>각 사건에서 &ldquo;깨졌던 규칙&quot;을 한 문장으로 쓴다.</li>
<li>그 규칙이 입력 검증, DB 제약, 도메인 서비스, reconciliation 중 어디에서 빠졌는지 표시한다.</li>
<li>고객 영향과 복구 비용으로 P0/P1/P2를 정한다.</li>
<li>detection query와 correction path가 없는 P0/P1부터 registry에 올린다.</li>
</ol>
<p>이 방식은 완벽한 분류표를 만드는 시간을 줄여 줍니다. 운영 비용을 이미 만든 규칙부터 계약으로 바꾸기 때문에 팀이 바로 필요성을 느낍니다. 나중에 도메인별 coverage를 넓히면 됩니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>첫째, registry는 너무 크게 시작하면 유지되지 않습니다. &ldquo;모든 비즈니스 규칙&quot;이 아니라 &ldquo;깨졌을 때 운영 사고가 되는 규칙&quot;부터 등록해야 합니다.</p>
<p>둘째, P0를 너무 많이 만들면 모두가 P0가 아닙니다. 한 건이라도 즉시 고객 피해, 보안 사고, 금전 불일치로 이어지는 규칙만 P0로 둡니다. 나머지는 허용 지연과 보정 경로를 숫자로 정의하는 편이 더 현실적입니다.</p>
<p>셋째, detection query가 source of truth를 잘못 잡으면 오탐이나 오보정이 생깁니다. 특히 파생 테이블끼리 비교하면 둘 다 틀릴 수 있습니다. 핵심 규칙은 원장, 이벤트 로그, 감사 로그처럼 더 신뢰도 높은 기준과 비교해야 합니다.</p>
<p>넷째, 불변식 위반을 자동 보정할 때는 오보정 비용을 봐야 합니다. 위반 100건을 자동으로 고치는 것보다 잘못 고친 1건이 더 비쌀 수 있습니다. 금전, 권한, 개인정보는 자동 보정보다 승인 보정이 기본입니다.</p>
<p>의사결정 우선순위는 <strong>고객 피해 차단 &gt; source of truth 고정 &gt; 쓰기 경로 방어 &gt; 탐지 신뢰도 &gt; 보정 속도</strong>입니다. 빠른 알람과 빠른 보정은 이 순서를 지킬 때만 의미가 있습니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<h3 id="체크리스트">체크리스트</h3>
<ul>
<li><input disabled="" type="checkbox"> 금전, 권한, 개인정보, 재고, 공개 상태에서 핵심 불변식을 각각 2개 이상 골랐다.</li>
<li><input disabled="" type="checkbox"> 각 불변식에 owner, severity, source of truth, detection query가 있다.</li>
<li><input disabled="" type="checkbox"> P0/P1/P2별 허용 지연과 알람 기준이 숫자로 정의돼 있다.</li>
<li><input disabled="" type="checkbox"> 불변식마다 보장 위치가 DB, 도메인 서비스, reconciliation, correction job 중 어디인지 명시돼 있다.</li>
<li><input disabled="" type="checkbox"> 위반 발생 시 자동 보정, 승인 보정, 동결 중 어떤 경로로 갈지 정해져 있다.</li>
<li><input disabled="" type="checkbox"> 고위험 PR 템플릿에 invariant delta 질문이 포함돼 있다.</li>
<li><input disabled="" type="checkbox"> 같은 invariant 재발 시 재발 방지 ticket이 자동 생성된다.</li>
</ul>
<h3 id="연습">연습</h3>
<ol>
<li>현재 서비스에서 &ldquo;깨지면 가장 비싼 데이터 규칙&rdquo; 5개를 적고 P0/P1/P2로 나눠 보세요. 금액, 권한, 개인정보가 포함되면 왜 그 등급인지 숫자로 설명합니다.</li>
<li>주문 도메인의 <code>CANCELED 주문은 배송 생성 불가</code> 규칙을 registry 항목으로 작성해 보세요. 쓰기 경로 방어, detection query, correction path를 각각 분리합니다.</li>
<li>검색 인덱스 삭제 전파 규칙을 P1로 두고 <code>delete lag p95</code>, <code>residual count</code>, <code>alert_after</code> 기준을 정해 보세요.</li>
<li>최근 데이터 보정 사례 하나를 골라 어떤 invariant가 registry에 없어서 늦게 발견됐는지 역추적해 보세요.</li>
</ol>
]]></content:encoded></item></channel></rss>