<?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 Masking on jyukki's Blog</title><link>https://jyukki.com/tags/data-masking/</link><description>Recent content in Data Masking on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-KR</language><lastBuildDate>Tue, 18 Aug 2026 10:06:00 +0900</lastBuildDate><atom:link href="https://jyukki.com/tags/data-masking/index.xml" rel="self" type="application/rss+xml"/><item><title>백엔드 커리큘럼 심화: 테스트 데이터 계약, 합성 seed·마스킹 사본·재현 환경을 고르는 법</title><link>https://jyukki.com/learning/deep-dive/deep-dive-test-data-contract-masking-synthetic-seed-playbook/</link><pubDate>Tue, 18 Aug 2026 10:06:00 +0900</pubDate><guid>https://jyukki.com/learning/deep-dive/deep-dive-test-data-contract-masking-synthetic-seed-playbook/</guid><description>통합 테스트·부하 테스트·장애 재현에서 합성 seed, 마스킹 사본, 고정 fixture를 어떻게 고르고 운영할지 데이터 계약과 숫자 기준으로 정리합니다.</description><content:encoded><![CDATA[<p>통합 테스트가 로컬에서는 통과하고 CI나 장애 재현 환경에서는 흔들리는 이유를 코드 탓으로만 돌리기 쉽습니다. 그러나 실제 원인은 데이터인 경우가 많습니다. 누군가는 <code>users</code> 테이블에 관리자 계정이 하나 있을 것이라 가정하고, 누군가는 어제의 상태를 기대하며, 테스트 실행 순서가 바뀌면 이미 소비한 쿠폰이나 만료된 토큰을 다시 사용합니다. 이 상태에서 운영 데이터를 조금 복사해 넣으면 당장은 편해 보여도, 개인정보 노출·오래된 스키마·비결정적 결과라는 더 큰 비용이 따라옵니다.</p>
<p>테스트 데이터는 샘플 레코드 모음이 아니라 <strong>테스트가 믿어도 되는 세계의 경계</strong>입니다. 이 글에서는 <a href="/learning/deep-dive/deep-dive-testcontainers-integration/">Testcontainers 통합 테스트</a>, <a href="/learning/deep-dive/deep-dive-load-testing-strategy/">부하 테스트 전략</a>, <a href="/learning/deep-dive/deep-dive-data-retention-deletion-architecture/">데이터 보존·삭제 아키텍처</a>와 연결해, 데이터의 출처·형태·초기화·보존을 하나의 계약으로 다루는 방법을 정리합니다.</p>
<h2 id="이-글에서-얻는-것">이 글에서 얻는 것</h2>
<ul>
<li>합성 seed, 고정 fixture, 마스킹한 운영 사본 중 무엇을 기본값으로 삼아야 하는지 판단할 수 있습니다.</li>
<li>테스트 데이터 계약에 반드시 넣을 스키마 버전, 불변식, 초기화, 소유자, 보존 기간을 정리할 수 있습니다.</li>
<li>개인정보를 가린 뒤에도 원본 데이터를 무심코 안전하다고 판단하지 않도록 위험을 분류할 수 있습니다.</li>
<li>CI·preview·부하·장애 재현 환경에 같은 데이터를 억지로 쓰지 않고 목적별로 분리하는 기준을 얻습니다.</li>
</ul>
<h2 id="핵심-개념이슈">핵심 개념/이슈</h2>
<h3 id="1-테스트-데이터-계약은-insert-파일보다-넓다">1) 테스트 데이터 계약은 <code>INSERT</code> 파일보다 넓다</h3>
<p>테스트가 필요한 것은 행 몇 개가 아닙니다. 예를 들어 주문 취소를 검증하려면 주문 상태만 아니라 결제 승인 시각, 재고 예약, 쿠폰 사용 여부, 환불 원장, 알림 발송 이력 사이의 관계가 맞아야 합니다. 따라서 계약은 “어떤 테이블에 무엇을 넣는가”보다 “이 데이터가 어떤 업무 사실을 표현하며, 테스트 뒤 어디까지 되돌아가야 하는가”를 먼저 말해야 합니다.</p>
<p>최소 계약에는 아래 항목을 둡니다.</p>
<table>
  <thead>
      <tr>
          <th>항목</th>
          <th>예시</th>
          <th>없을 때 생기는 문제</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>목적</td>
          <td><code>주문 취소 후 재고 복원</code></td>
          <td>데이터가 왜 필요한지 몰라 불필요한 필드가 늘어남</td>
      </tr>
      <tr>
          <td>스키마 버전</td>
          <td><code>V20260818_03</code></td>
          <td>migration 뒤 CI만 조용히 오래된 구조를 사용</td>
      </tr>
      <tr>
          <td>불변식</td>
          <td><code>paid_amount = captured_amount - refunded_amount</code></td>
          <td>그럴듯하지만 불가능한 상태를 fixture가 허용</td>
      </tr>
      <tr>
          <td>초기화 방식</td>
          <td>transaction rollback, schema recreate, namespace delete</td>
          <td>실행 순서에 따라 결과가 달라짐</td>
      </tr>
      <tr>
          <td>데이터 등급</td>
          <td>synthetic, masked, restricted</td>
          <td>운영 데이터가 일반 fixture처럼 복제됨</td>
      </tr>
      <tr>
          <td>owner·만료일</td>
          <td>payments-platform, 30일</td>
          <td>변경·삭제 책임자가 사라짐</td>
      </tr>
  </tbody>
</table>
<p>이 계약은 <a href="/learning/deep-dive/deep-dive-consumer-driven-contract-testing/">Consumer-Driven Contract Testing</a>의 API 계약과 비슷합니다. API 입력·출력만 고정하고 그 입력이 성립하는 데이터 세계를 방치하면, 계약 테스트는 통과해도 실제 조합에서 실패합니다. 반대로 모든 운영 상태를 복제하려 하면 테스트가 느리고 설명하기 어려워집니다. 핵심은 필요한 사실만 작고 명시적으로 만드는 것입니다.</p>
<h3 id="2-기본값은-결정론적-합성-seed다">2) 기본값은 결정론적 합성 seed다</h3>
<p>대부분의 단위·통합·preview 테스트에서는 합성 seed가 가장 안전한 출발점입니다. 사람 이름, 이메일, 카드 번호처럼 보이는 값도 실제 개인과 연결되지 않는 생성 규칙을 사용하고, 시간·난수·ID를 주입 가능하게 만듭니다. 같은 commit과 같은 seed라면 어느 개발자 머신과 CI에서도 같은 결과가 나와야 합니다.</p>
<p>합성 데이터가 특히 적합한 경우는 다음과 같습니다.</p>
<ul>
<li>상태 전이, 권한, 정산처럼 <strong>불변식</strong>이 더 중요한 테스트</li>
<li>PR마다 실행되어 초기화 시간이 10분을 넘으면 개발 흐름을 막는 테스트</li>
<li>고객 식별자나 주문 내용이 없어도 도메인 규칙을 표현할 수 있는 테스트</li>
<li>오류 입력을 의도적으로 만들어야 하는 negative test</li>
</ul>
<p>예를 들어 <code>order-1001</code>을 만드는 것보다 <code>결제 완료·부분 환불 1회·배송 전·쿠폰 사용</code>이라는 의미 있는 builder를 둡니다. 이때 식별자는 난수 UUID 대신 고정 prefix와 증가 번호를 써서 실패 로그가 비교 가능하게 만듭니다. 현재 시각은 <code>Clock</code> 또는 test fixture의 기준 시각으로 주입합니다. 날짜가 바뀌어 만료 테스트가 깨지는 상황을 피할 수 있습니다.</p>
<h3 id="3-마스킹한-운영-사본은-안전한-기본-fixture가-아니다">3) 마스킹한 운영 사본은 ‘안전한 기본 fixture’가 아니다</h3>
<p>실제 분포, 희귀한 조합, 오래 축적된 상태를 재현해야 할 때는 마스킹 사본이 도움이 됩니다. 하지만 이름과 이메일을 바꿨다고 해서 안전해지는 것은 아닙니다. 우편번호·생년월일·희귀 구매 이력·내부 ID의 조합은 다시 식별될 수 있고, 주문 금액이나 사용 패턴 자체가 민감한 사업 정보일 수도 있습니다. 토큰·비밀번호 hash·세션·웹훅 서명처럼 재사용 가능하거나 비밀에 가까운 값은 마스킹 대상이 아니라 <strong>반출 금지 대상</strong>입니다.</p>
<p>마스킹 사본은 다음 네 조건을 모두 만족할 때만 예외적으로 사용합니다.</p>
<ol>
<li>합성 seed로 재현할 수 없는 실제 분포 또는 결함 가설이 문서화되어 있다.</li>
<li>필요한 테이블·기간·컬럼만 추출했으며, 직접·간접 식별자와 credential은 제거 또는 비가역 변환했다.</li>
<li>승인된 격리 환경에서만 암호화해 보관하고, 접근 로그와 자동 만료가 있다.</li>
<li>재현 실험이 끝난 뒤 삭제 또는 재생성할 수 있으며, 일반 CI fixture 저장소에는 들어가지 않는다.</li>
</ol>
<p>즉 “운영 데이터를 조금 쓰자”는 요청은 데이터 편의성 문제가 아니라 별도 보안·보존 결정입니다. 데이터 보존 정책이 30일이라도 장애 재현 사본을 180일 두면, 테스트 시스템이 새 보관 시스템이 됩니다.</p>
<h3 id="4-환경마다-성공-기준이-다르다">4) 환경마다 성공 기준이 다르다</h3>
<p>같은 데이터 파일 하나로 모든 테스트를 해결하려 하면 대개 실패합니다. 빠른 피드백이 목적이면 작고 고정된 데이터가, 성능·분포 검증이 목적이면 규모와 편차가 필요합니다.</p>
<table>
  <thead>
      <tr>
          <th>환경</th>
          <th>우선순위</th>
          <th>권장 데이터</th>
          <th>시작 기준</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>로컬·PR CI</td>
          <td>결정성, 속도, 격리</td>
          <td>합성 seed + 최소 fixture</td>
          <td>95% 이상이 독립 실행에서 재현, 초기화 p95 2분 이내</td>
      </tr>
      <tr>
          <td>preview·통합</td>
          <td>서비스 경계, 연동 계약</td>
          <td>합성 도메인 시나리오 + stub</td>
          <td>외부 전송은 mock 또는 no-send route로 차단</td>
      </tr>
      <tr>
          <td>부하 테스트</td>
          <td>분포, hot key, 용량</td>
          <td>생성기 기반 대량 synthetic data</td>
          <td>실제 고객 식별자 없이 skew·카디널리티를 모델링</td>
      </tr>
      <tr>
          <td>장애 재현</td>
          <td>원인 재현, 증거 보존</td>
          <td>승인된 좁은 masked snapshot</td>
          <td>만료일·접근자·삭제 절차가 티켓에 연결</td>
      </tr>
  </tbody>
</table>
<p>여기서 숫자는 절대 규칙이 아니라 출발점입니다. PR CI 초기화 p95가 2분을 넘는다면, 개발자는 테스트를 줄이거나 재실행을 포기하기 시작합니다. 반대로 부하 테스트 데이터를 너무 작게 만들면 인덱스 선택, 캐시 hit ratio, 큐 파티션 편향이 실제와 전혀 다르게 보입니다. <a href="/learning/deep-dive/deep-dive-cache-warmup-cold-start-playbook/">캐시 워밍업과 cold start</a>처럼 규모가 성능 결론을 바꾸는 경로는 작은 fixture의 결과를 production 용량 판단에 직접 쓰면 안 됩니다.</p>
<h2 id="실무-적용">실무 적용</h2>
<h3 id="1-데이터-생성-코드를-제품-코드처럼-관리한다">1) ‘데이터 생성 코드’를 제품 코드처럼 관리한다</h3>
<p><code>testdata.sql</code> 하나에 수천 줄을 쌓기보다, 도메인별 builder와 명시적 scenario를 둡니다. 예를 들면 <code>createPaidOrder()</code>, <code>createExpiredReservation()</code>, <code>createSuspendedTenant()</code>처럼 이름만으로 업무 상태가 드러나게 합니다. builder는 기본적으로 유효한 상태를 만들고, 불가능한 상태가 필요한 test만 <code>override</code>를 명시합니다. 이렇게 해야 fixture 자체가 규칙을 어기는 일을 줄일 수 있습니다.</p>
<p>저장소에는 다음 정도의 구조가 실용적입니다.</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-text" data-lang="text"><span style="display:flex;"><span>test-support/
</span></span><span style="display:flex;"><span>  scenarios/          # 결제·권한·주문 같은 읽기 가능한 업무 시나리오
</span></span><span style="display:flex;"><span>  builders/           # 유효한 기본 상태를 만드는 코드
</span></span><span style="display:flex;"><span>  contracts/          # 불변식, schema version, owner, retention
</span></span><span style="display:flex;"><span>  generators/         # 부하 테스트용 대량 합성 생성기
</span></span><span style="display:flex;"><span>  masking/             # 승인 환경에서만 실행하는 변환 규칙
</span></span></code></pre></div><p>schema migration이 바뀌면 CI가 계약의 <code>schema_version</code>과 생성 결과를 같이 검증하게 합니다. fixture만 고치고 migration은 잊는 경우를 막기 위해, migration PR에는 영향받는 scenario 이름을 적게 하는 것도 좋습니다. <code>schema recreate</code>가 어려운 공유 환경에서는 tenant namespace 또는 run ID를 붙이되, cleanup 실패가 새 데이터로 덮이지 않도록 만료 worker와 잔존 데이터 alert를 둡니다.</p>
<h3 id="2-불변식과-데이터-품질-검사를-테스트-전에-실행한다">2) 불변식과 데이터 품질 검사를 테스트 전에 실행한다</h3>
<p>테스트가 시작된 뒤 20분 만에 “필수 계정이 없어서 실패”하는 것보다, seed 직후 5초 안에 잘못된 데이터를 알려 주는 편이 낫습니다. 최소한 다음 검사를 별도 단계로 둡니다.</p>
<ul>
<li>참조 무결성: 고아 주문, 존재하지 않는 tenant를 참조하는 권한이 없는가</li>
<li>합계 불변식: 결제·환불·잔액·재고 예약이 서로 맞는가</li>
<li>시간 불변식: <code>created_at &lt;= updated_at</code>, 만료 전 상태와 만료 후 상태가 섞이지 않는가</li>
<li>보안 불변식: 실제 이메일 도메인, access token 형식, production API host가 섞이지 않았는가</li>
<li>격리 불변식: 실행 ID가 다른 데이터와 교차 조회되지 않는가</li>
</ul>
<p>이 검사는 테스트 대상 기능의 정답을 확인하는 단위 테스트와 다릅니다. <strong>테스트 입력 자체가 신뢰 가능한지</strong> 확인하는 gate입니다. 불변식이 깨진 fixture를 고치지 않고 앱 코드만 맞추면, 언젠가 다른 테스트에서 같은 거짓 가정이 다시 문제를 만듭니다.</p>
<h3 id="3-마스킹-파이프라인에는-원본-검증보다-반출-검증을-우선한다">3) 마스킹 파이프라인에는 원본 검증보다 반출 검증을 우선한다</h3>
<p>마스킹 job은 “잘 바꿨다”보다 “나가면 안 되는 것이 전혀 남지 않았다”를 먼저 확인해야 합니다. export query를 allowlist로 고정하고, 새 컬럼이 추가되면 자동 포함되지 않게 합니다. 대상은 90일치 전체처럼 넓게 잡지 말고 재현에 필요한 기간·tenant·레코드 종류로 제한합니다. 생성한 파일은 checksum과 manifest를 남기되, manifest에도 원본 식별자나 민감한 집계값을 넣지 않습니다.</p>
<p>검증은 샘플 20건을 사람이 읽는 절차와, 패턴·도메인·secret scanner를 통한 자동 검사를 함께 둡니다. 둘 중 하나만으로는 부족합니다. 사람이 보기에는 정상이어도 API key 형식이 남을 수 있고, 패턴 검사만으로는 조합 재식별 위험을 알아채기 어렵습니다. 실패한 export는 재사용하지 않고, 보관 위치·접근 로그·만료 정책을 먼저 점검합니다.</p>
<h2 id="트레이드오프주의점">트레이드오프/주의점</h2>
<p>합성 데이터는 안전하고 빠르지만, 실제 고객 분포의 긴 꼬리와 오래된 데이터의 이상한 조합을 놓칠 수 있습니다. 이를 이유로 운영 사본을 상시 쓰기 시작하면 반대로 테스트가 느려지고, 개인정보·접근권한·삭제 요청의 범위가 급격히 커집니다. 해결책은 둘 중 하나를 만능으로 삼는 것이 아니라, 일상적인 검증은 합성 데이터로 닫고 재현 가치가 큰 사건만 좁은 마스킹 절차로 올리는 것입니다.</p>
<p>또한 결정론은 같은 입력에서 같은 결과가 난다는 뜻이지, 현실을 충분히 닮았다는 뜻은 아닙니다. 가격이 0원인 주문, 1개의 tenant, 균등한 키 분포만 가진 fixture는 항상 통과하지만 production의 hot tenant, 큰 payload, 오래된 migration 경로에서는 무력합니다. 부하 생성기에는 평균값만 아니라 상위 1%의 큰 주문·긴 목록·특정 키 집중도를 의도적으로 넣어야 합니다.</p>
<p>마지막으로 데이터 reset을 <code>TRUNCATE</code> 한 번으로 끝낼 수 있다고 가정하지 마세요. object storage, 검색 인덱스, 캐시, 비동기 큐, 외부 mock 서버는 DB 롤백 밖에 있을 수 있습니다. <a href="/learning/deep-dive/deep-dive-webhook-delivery-reliability-playbook/">웹훅 전달 신뢰성</a>처럼 외부 효과가 있는 테스트는 no-send endpoint, 독립 topic, run ID, cleanup 증적을 함께 가져야 합니다.</p>
<h2 id="체크리스트-또는-연습">체크리스트 또는 연습</h2>
<p>이번 주에는 자주 깨지는 통합 테스트 하나를 골라 다음을 작성해 보세요.</p>
<ul>
<li><input disabled="" type="checkbox"> 이 테스트가 재현해야 하는 업무 사실을 한 문장으로 적었는가?</li>
<li><input disabled="" type="checkbox"> schema version, owner, 데이터 등급, 만료일, 초기화 방법이 계약에 있는가?</li>
<li><input disabled="" type="checkbox"> 고정된 시각·난수 seed·의미 있는 builder로 독립 실행을 재현할 수 있는가?</li>
<li><input disabled="" type="checkbox"> seed 뒤 참조 무결성·합계·시간·보안 불변식을 1분 안에 검증하는가?</li>
<li><input disabled="" type="checkbox"> 실제 고객 식별자, secret, production URL이 fixture에 없음을 자동 검사하는가?</li>
<li><input disabled="" type="checkbox"> 부하 테스트는 평균 데이터뿐 아니라 hot key와 큰 payload 분포를 포함하는가?</li>
<li><input disabled="" type="checkbox"> 마스킹 사본이 있다면 추출 사유, 승인자, 접근 로그, 자동 만료, 삭제 절차가 연결되어 있는가?</li>
</ul>
<p>테스트 데이터의 완성도는 레코드 수가 아니라 <strong>실패를 같은 방식으로 다시 만들 수 있는가, 그리고 그 과정에서 보호하면 안 되는 정보를 만들지 않는가</strong>로 평가해야 합니다. 계약을 작게 시작해도 이 두 질문에 답할 수 있으면, 테스트는 우연히 통과하는 스크립트에서 운영 판단을 지지하는 자산으로 바뀝니다.</p>
]]></content:encoded></item></channel></rss>