AI로 테스트 픽스처·시드 데이터 만들기: 고정 시드 팩토리와 개인정보 검사
같은 시드면 같은 데이터, 경계값은 명시적으로, 실제 개인정보는 커밋 전에 걸러내기

핵심 요약
- 좋은 테스트 데이터는 재현 가능하고, 경계값을 포함하며, 실제 개인정보가 없어야 한다.
- Math.random 대신 시드를 받는 난수 생성기로 팩토리를 만들면 같은 시드에서 같은 데이터(같은 해시)가 나온다.
- 이메일은 RFC 2606 예약 도메인(.test, example.com 등)만 써서 실제 주소와 겹치지 않게 한다.
- 정규식 개인정보 검사는 형식 있는 값만 잡으므로, 운영 데이터를 출발점으로 삼지 않는 원칙이 먼저다.
테스트가 가끔씩만 실패한다면 원인이 코드가 아니라 데이터일 때가 많다. 실행할 때마다 이름이나 금액이 바뀌고, 운영 데이터를 복사해 왔더니 개인정보가 섞여 있고, 경계값이 빠져 버그가 배포 후에야 드러난다. AI는 스키마를 보고 그럴듯한 데이터를 빠르게 만들어 주지만, 그대로 쓰면 이 세 문제가 그대로 남는다. 이 글은 재현 가능, 경계값 포함, 개인정보 없음이라는 세 조건을 코드로 고정하는 방법을 실제 실행 결과로 정리한다.
운영 데이터를 복사하면 생기는 문제
| 문제 | 내용 |
|---|---|
| 개인정보 확산 | 덤프 파일이 개발자 노트북, CI 로그, 스테이징, 백업으로 퍼진다 |
| 삭제 요청 누락 | 운영에서 삭제된 사용자가 테스트 데이터에는 남는다 |
| 재현 불가 | 운영 데이터가 계속 바뀌어 같은 테스트를 반복할 수 없다 |
| 느린 테스트 | 필요 없이 큰 데이터가 실행 시간을 늘린다 |
대안은 스키마와 관계만 유지하고 값은 새로 만드는 것이다. 이때 AI에 바로 데이터를 만들게 하기보다, 먼저 "이 필드의 정상·경계·실패 입력을 표로 정리하라"고 요청해 목록부터 받는다.
만드는 순서
- AI에 스키마와 도메인 규칙을 주고 필드별 정상·경계·실패 입력 목록을 표로 받는다.
- 정상 데이터는 고정 시드 팩토리로 만들고, 경계값은 목록으로 명시해 따로 고정한다.
- 이메일은 RFC 2606 예약 도메인만 쓰고, 전화번호·식별번호는 실제 형식과 겹치지 않는 값으로 만든다.
- 픽스처 파일을 커밋하기 전에 개인정보 검사를 실행한다(커밋 훅이나 CI).
- 같은 시드로 두 번 생성해 결과 해시가 같은지 확인한다.
고정 시드 팩토리와 경계값 목록
Math.random()은 시드를 줄 수 없어 실행마다 결과가 달라진다. 아래는 시드를 받는 작은 난수 생성기(mulberry32)로 팩토리를 만든 예다. 이메일은 RFC 2606이 테스트용으로 예약한 .test 도메인을 쓴다. 이 문서는 .test, .example, .invalid, .localhost 최상위 도메인과 example.com/net/org를 예약해 두었으므로 실제 주소와 겹치지 않는다.
// 고정 시드 팩토리: 같은 시드면 언제 어디서 실행해도 같은 데이터가 나온다(Math.random 사용 안 함).
export function mulberry32(seed) {
return function () {
seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export function createFactory(seed) {
const rnd = mulberry32(seed);
const int = (min, max) => min + Math.floor(rnd() * (max - min + 1));
let seq = 0;
return {
user(overrides = {}) {
seq += 1;
return {
id: `u_${String(seq).padStart(4, '0')}`,
// RFC 2606 예약 도메인(.test)만 써서 실제 주소와 겹치지 않게 한다
email: `user${int(1000, 9999)}@example.test`,
name: `테스트사용자${seq}`,
plan: ['free', 'pro', 'team'][int(0, 2)],
balance: int(0, 500000),
...overrides,
};
},
};
}
// 정상 데이터와 별도로, 규칙이 뒤집히는 지점을 명시적으로 고정한다
export const BOUNDARY_USERS = [
{ label: '잔액 0원', balance: 0 },
{ label: '무료 배송 기준 직전', balance: 49999 },
{ label: '무료 배송 기준', balance: 50000 },
{ label: '이름 최대 길이(80자)', name: '가'.repeat(80) },
{ label: '이름 길이 초과(81자)', name: '가'.repeat(81), expectError: true },
];import { writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { createFactory, BOUNDARY_USERS } from './factory.mjs';
const seed = Number(process.argv[2] || 20261009);
const f = createFactory(seed);
const users = [...Array.from({ length: 5 }, () => f.user()), ...BOUNDARY_USERS.map(({ label, ...o }) => ({ ...f.user(o), _case: label }))];
const json = JSON.stringify(users, null, 2);
writeFileSync('users.fixture.json', json);
console.log(`seed ${seed} → ${users.length}건, sha1 ${createHash('sha1').update(json).digest('hex').slice(0, 12)}`);
console.log(users.slice(0, 2).map((u) => JSON.stringify(u)).join('\n'));$ node make-fixtures.mjs 20261009 # 2026-10-09
seed 20261009 → 10건, sha1 16af59ed4ad7
{"id":"u_0001","email":"[email protected]","name":"테스트사용자1","plan":"pro","balance":20525}
{"id":"u_0002","email":"[email protected]","name":"테스트사용자2","plan":"pro","balance":44297}$ node make-fixtures.mjs 20261009 | head -1; node make-fixtures.mjs 7 | head -1
seed 20261009 → 10건, sha1 16af59ed4ad7
seed 7 → 10건, sha1 a5ab9b062fa5같은 시드는 같은 해시를, 다른 시드는 다른 해시를 냈다. 테스트가 실패했을 때 시드를 로그에 남겨 두면 같은 데이터로 바로 재현할 수 있다. 경계값(잔액 0원, 기준 직전·기준, 이름 최대 길이와 초과)은 난수에 맡기지 않고 목록으로 고정해 매번 반드시 포함되게 했다.
개인정보 검사
AI가 만든 데이터에도 실제처럼 보이는 이메일이나 번호가 섞일 수 있고, 급할 때 운영 데이터를 복사해 붙이는 일도 생긴다. 아래 스크립트는 예약 도메인이 아닌 이메일, 휴대전화·주민등록번호·카드번호 형식을 찾고, 출력할 때도 값을 가린다.
// 사용법: node pii-scan.mjs <픽스처 파일...>
// 픽스처에 실제 개인정보처럼 보이는 값이 섞였는지 확인한다. 예약 도메인(.test/.example/example.com 등)은 통과시킨다.
import { readFileSync } from 'node:fs';
const RULES = [
['이메일(예약 도메인 아님)', /[\w.+-]+@(?![\w-]*\.?example\.(com|net|org)\b)(?![\w.-]+\.(test|example|invalid)\b)[\w-]+(\.[\w-]+)+/g],
['휴대전화 번호', /\b01[016789]-?\d{3,4}-?\d{4}\b/g],
['주민등록번호 형식', /\b\d{6}-?[1-4]\d{6}\b/g],
['카드번호 형식', /\b(?:\d{4}[- ]?){3}\d{4}\b/g],
];
let found = 0;
for (const file of process.argv.slice(2)) {
const text = readFileSync(file, 'utf8');
const hits = RULES.flatMap(([name, re]) => [...text.matchAll(re)].map((m) => `${name}: ${m[0].replace(/(?<=.{3})./g, '*')}`));
found += hits.length;
console.log(`${hits.length ? 'FAIL' : 'PASS'} ${file}`);
hits.forEach((h) => console.log(` - ${h}`));
}
process.exitCode = found ? 1 : 0;팩토리로 만든 파일과, 운영 데이터를 복사했다고 가정하고 만든 예시 파일(copied-from-prod.json, 값은 지어낸 것)을 함께 검사했다.
$ node pii-scan.mjs users.fixture.json copied-from-prod.json
PASS users.fixture.json
FAIL copied-from-prod.json
- 이메일(예약 도메인 아님): min*****************
- 휴대전화 번호: 010**********
exit=1[email protected]은 예약 도메인이라 통과했고, 일반 메일 주소와 휴대전화 형식은 걸렸다. 정규식 검사는 이름이나 주소처럼 형식이 없는 개인정보는 찾지 못하므로, 운영 데이터를 출발점으로 삼지 않는 원칙이 먼저다. 부득이 운영 데이터를 써야 한다면 복구할 수 없게 마스킹하고 원본과의 매핑을 저장하지 않는다. 마스킹 설계는 LLM 호출 전 개인정보 마스킹을 참고한다.
유지 규칙
- 스키마가 바뀌면 팩토리 기본값 한 곳만 고치면 되도록 구성한다.
- 픽스처 이름에 목적을 담는다(예:
userWithExpiredCard). - 리뷰에서는 새 픽스처가 팩토리를 재사용하는지, 개인정보 검사를 통과하는지 확인한다.
- 테스트 실패 로그에 시드 값을 남긴다.
개인정보 검사를 커밋 전에 자동으로 실행하는 방법은 커밋 전 검사 자동화, 경계값을 테스트로 먼저 쓰는 흐름은 AI 코딩 어시스턴트와 TDD에서 다룬다.
초보자가 자주 실수하는 포인트
- 운영 DB 덤프를 그대로 테스트 데이터로 쓴다.
- Math.random으로 데이터를 만들어 실패를 재현하지 못한다.
- 경계값을 난수에 맡겨 매번 포함되지 않는다.
- AI가 만든 그럴듯한 이메일·번호를 검사 없이 커밋한다.
체크리스트
- 필드별 정상·경계·실패 목록을 표로 정리했다.
- 팩토리가 시드를 받고, 같은 시드로 같은 결과가 나온다.
- 경계값이 목록으로 고정되어 있다.
- 이메일이 예약 도메인만 쓴다.
- 픽스처 커밋 전에 개인정보 검사를 실행한다.
자주 묻는 질문
Faker 같은 라이브러리를 써도 되나요?
네. 대부분의 데이터 생성 라이브러리는 시드를 설정할 수 있습니다. 시드를 고정하고, 이메일 도메인 같은 값은 예약 도메인으로 바꾸는 설정을 확인하세요.
AI에 직접 JSON 픽스처를 만들게 하면 안 되나요?
초안으로는 쓸 수 있지만, 생성할 때마다 결과가 달라지고 실제처럼 보이는 값이 섞일 수 있습니다. 한 번 만든 결과를 파일로 고정하고 개인정보 검사와 스키마 검증을 통과시킨 뒤 커밋하는 것이 안전합니다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.