
운영 데이터를 복사하여 테스트하는 방식에서 벗어나기 — LLM으로 '안전한 합성 테스트 데이터'를 만드는 실전 가이드 (PII 미포함·스키마
요약
운영 데이터를 복사하는 위험한 방식 대신, LLM을 활용하여 개인정보(PII)가 포함되지 않은 안전한 합성 테스트 데이터를 생성하는 실전 가이드를 제공합니다. 스키마와 규칙을 기반으로 데이터의 관계성과 자유 기술을 정교하게 구현하는 방법론을 다룹니다.
핵심 포인트
- 운영 데이터 복사는 개인정보 유출 위험을 높이므로 지양해야 함
- 합성 데이터는 실존 데이터를 복사하는 것이 아니라 스키마 기반으로 생성함
- 단순 데이터는 faker를, 관계성 및 문장 데이터는 LLM을 활용해 역할 분담
- 데이터 생성 시 실존 개인정보를 프롬프트에 입력하지 않는 것이 철칙
테스트를 할 때마다 운영 DB를 복사해서 staging에 통째로 흘려보내고 계시지는 않나요?
솔직히 말씀드리겠습니다. 저도 예전에는 그랬습니다. "운영 데이터와 똑같지 않으면 버그가 재현되지 않으니까"라고 말이죠. 하지만 그것은 사실 꽤 위험한 줄타기를 하는 것입니다. 운영의 사용자 테이블에는 실존하는 사람의 이름, 이메일 주소, 전화번호, 주소가 들어있습니다. 그것을 그대로 검증 환경에 복사하는 순간, 그 환경은 '개인정보가 포함된 환경'으로 격상되어 버립니다. 액세스 권한이 넓은 dev 환경, 로그가 남는 CI, 자칫하면 외주 업체도 볼 수 있는 staging...
운영 데이터를 복사할 때마다, 개인정보의 "놓여있는 장소"가 조용히 늘어나고 있다는 이야기입니다.
오늘은 이 「운영 복사 문제」를 졸업하기 위한 실천적인 방법을, LLM (대규모 언어 모델)을 사용하여 만드는 「합성 테스트 데이터 (Synthetic Test Data)」라는 관점에서 코드와 함께 친절하게 공유해 드리겠습니다. 무지의 무지 상태, 즉 「합성 데이터라는 말조차 처음 들어봤다」는 분들도 읽어나갈 수 있도록 용어를 하나씩 풀어서 설명해 드릴게요.
결론부터 말씀드리겠습니다.
- 테스트 데이터는 운영에서 복사하는 것이 아니라,
스키마 (Schema, 표의 설계도)와 규칙으로부터 "제로에서부터 만든다". 이것이 합성 테스트 데이터의 발상입니다. - 만들 때,
실존하는 개인의 데이터를 LLM의 프롬프트에 붙여넣지 않는다. 이것은 절대적인 철칙입니다. 합성 데이터는 「진짜와 똑같은 가짜」를 만드는 기술이지, 「진짜를 가져오는」 기술이 아닙니다. - 그리고,
단순한 컬럼은 faker, 관계성이나 자유 기술은 LLM, 어디까지 진짜처럼 만들지·무엇을 합격으로 볼지는 인간이 결정한다. 이 역할 분담이 전체의 뼈대가 됩니다.
이 세 가지를 머릿속에 담아두고 다음 내용을 읽어주시면 감사하겠습니다.
기술 기사는 용어에서 막히면 단번에 "나에게는 너무 이르다"라고 느끼게 됩니다. 그래서 먼저 이 기사에 등장하는 용어들을 일상적인 비유로 정리해 두겠습니다.
- 합성 데이터 (Synthetic Data): 실재하는 기록을 복사하는 것이 아니라, 통계적인 특징이나 규칙만을 흉내 내어 인공적으로 만든 데이터. 초상화가 아니라 「몽타주」 같은 것입니다. 본인은 존재하지 않습니다.
- PII (Personally Identifiable Information, 개인 식별 정보): 이름, 이메일, 전화번호, 주소, 신용카드 번호 등 그 사람을 특정할 수 있는 정보. 보호해야 할 대상의 주인공입니다. GDPR (EU의 개인정보 보호 규정)이나 HIPAA (미국의 의료정보 보호법)가 "비운영 환경에서도 지켜라"라고 말하는 대상입니다.
- 스키마 (Schema): 테이블의 설계도. "이 컬럼은 정수", "이것은 필수", "이것은 0 이상"과 같은 타입과 제약 조건의 집합입니다.
- 참조 무결성 (Referential Integrity): 주문 테이블의
user_id가 사용자 테이블에 실제로 존재하는 ID를 제대로 가리키고 있는 것. 제각각으로 만들면 이 「연결성」이 깨집니다. - 결정성 (Determinism): 동일한 입력으로부터 동일한 결과가 나오는 것. 테스트에서 가장 중요한 성질입니다. 매번 다른 데이터가 나오면 테스트가 불안정해집니다.
이 다섯 가지를 알고 있다면 충분히 따라오실 수 있습니다.
"더미 데이터라면 faker를 쓰면 되잖아?"라는 목소리가 들릴 것 같습니다. 절반은 정답입니다. faker는 이름 같은 문자열이나 그럴싸한 이메일 주소를 대량으로 뱉어내는 데는 엄청나게 빠릅니다. 하지만 faker 단독으로는 약점이 있습니다.
faker가 어려워하는 부분은 예를 들면 다음과 같습니다.
- 테이블을 넘나드는
관계성 ("프리미엄 회원만 청구 이력이 3건 이상 있다"와 같은 조건부 정합성) - 자유 기술 (상품 리뷰 본문, 문의 메일, 에러 설명문 같은 "문장")
- 다국어·문맥 의존 (일본어의 자연스러운 주소나 업계 용어 같은 말투)
이 부분이 바로 LLM의 차례입니다. LLM은 「그럴싸한 문장」과 「문맥에 맞는 값의 조합」을 만드는 데 능숙합니다. 반대로, 단순히 고유한 ID를 부여하거나 날짜를 일정한 간격으로 나열하는 것과 같은 기계적인 작업은 faker (또는 단순한 코드)가 더 빠르고 확실합니다.
그리고 잊어서는 안 될 것이 인간의 업무입니다. 어떤 컬럼을 진짜처럼 만들어야 할지, 어디까지 리얼해야 "테스트로서 의미가 있는지", 생성된 데이터를 운영에 준하는 환경에 넣어도 되는지——이 「판단」만큼은 인간이 쥐고 있어야 합니다. 이 부분을 AI에게 통째로 맡기면 조용히 사고가 납니다.
요약하자면, 다음과 같은 분담입니다.
| 업무 종류 | 담당 | 이유 |
|---|---|---|
| 고유 ID·일련번호·일정한 간격의 날짜 | faker / 단순 코드 | 기계적이고 확실하며 빠름 |
| ... | 비즈니스 판단·리스크 판단은 맡기지 않음 |
가장 중요한 핵심은 "스키마 퍼스트 (Schema-first)"입니다. 먼저 "어떤 형태의 데이터가 필요한가"를 틀(Schema)로 확실히 작성한 뒤, LLM에는 그 틀의 "내용물"만 채우게 하는 것입니다. 이렇게 하면 데이터 형식이 깨져서 돌아오는 사고를 획기적으로 줄일 수 있습니다.
Python의 Pydantic(타입을 정의하면 자동으로 유효성 검사를 해주는 라이브러리)을 사용하여 먼저 설계도를 작성합니다.
# schema.py — 원하는 데이터의 「설계도」를 먼저 확정함
from pydantic import BaseModel, Field
from typing import Literal
...
그다음, OpenAI의 "Structured Outputs (구조화된 출력)"를 사용하여 이 스키마에 딱 맞는 합성 사용자 데이터를 생성하게 합니다. Structured Outputs는 LLM의 응답을 "이 JSON 스키마에 엄격하게 (strict) 따르도록" 만드는 기능으로, Pydantic 모델을 그대로 전달할 수 있어 매우 편리합니다.
# generate.py — 스키마를 엄격하게 따르는 합성 데이터를 생성함
from openai import OpenAI
from pydantic import BaseModel
...
포인트는 LLM에게 "자유롭게 무언가 만들어줘"라고 부탁하지 않았다는 점입니다. 대신 "이 타입의 내용물만 채워줘"라고 요청합니다. 따라서 반환되는 데이터는 반드시 plan이 3가지 값 중 하나이거나, user_id가 1 이상인 것과 같은 "깨지지 않는 형태"의 데이터가 됩니다. 이 점이 바로 Structured Outputs를 사용할 때의 쾌감입니다.
솔직히 말씀드리면, 이 부분이 오늘 가장 전달하고 싶은 내용입니다. 합성 데이터 생성의 흔한 실패 사례는 "너무 깨끗하고 평균적인 데이터만 만들어버리는 것"입니다. 하지만 버그는 언제나 **가장자리 (Edge cases)**에서 발생합니다. 이름이 빈 문자열일 때, 이모지만 있을 때, 날짜가 미래일 때, 금액이 마이너스일 때와 같은 경우 말이죠.
따라서 생성 지시사항에 "의도적으로 가장자리를 노리도록" 하는 패턴을 포함합니다. 테스트에서 다뤄야 할 대표적인 7가지 패턴을 표로 정리해 두었습니다.
| # | 패턴 | 예시 |
|---|---|---|
| 1 | 정상계 (중앙값) | 일반적인 이름 · 타당한 금액 |
| ... | ... | ... |
이를 LLM에게 생성하도록 하는 프롬프트는 다음과 같습니다.
당신은 테스트 데이터 설계자입니다. 다음 스키마에 대해,
「정상계」뿐만 아니라, 아래의 경계값 · 이상계를 최소 1건씩 포함하는 데이터를 만들어주세요.
- 빈 문자열 / 최솟값 케이스
...
이렇게 해두면 나중에 "이상계 데이터만 추출하여 에러 핸들링을 테스트한다"와 같은 방식으로 활용할 수 있습니다. 평균적인 데이터를 1만 건 만드는 것보다, 타겟팅한 경계값을 7건 만드는 것이 훨씬 더 효과적으로 버그를 잡아냅니다.
이 부분은 절대 그냥 지나치지 마세요. 합성 데이터의 최대 목적은 "실제 PII (개인정보)를 테스트 환경에 들여오지 않는 것"입니다. 그런데 LLM이 실수로 실제 존재할 법한 이메일이나 유명인의 이름을 내뱉을 때가 있습니다. 그래서 "정말로 안전한가"를 사람의 눈이 아닌 기계로 검사합니다.
# pii_guard.py — 생성물에 실제 PII와 유사한 파편이 있는지 검사함
import re
# 더미로 허용할 도메인 (이외의 이메일은 의심함)
...
이를 생성 직후에 반드시 통과시킵니다. example.com 이외의 이메일이 나오면 중단하고, 휴대폰 번호와 유사한 패턴이 나오면 중단합니다. 여기서 중요한 것은 "의심스러우면 차단한다 (default deny)"는 태도입니다. "아마 괜찮겠지"라며 통과시킨 단 한 건이 나중에 대형 사고로 이어질 수 있습니다. 검사가 완벽할 필요는 없습니다. "명백하게 잘못된 형태"를 기계적으로 걸러내는 것만으로도 사고율은 급격히 낮아집니다.
그리고 또 하나의 철칙이 있습니다. 애초에 LLM의 프롬프트에 실제 운영 데이터를 붙여넣지 마세요. "운영 환경의 이 사용자 같은 데이터를 만들어줘"라며 실제 데이터를 전달하는 순간, 그것은 이미 "실제 데이터를 외부로 유출한 것"이 됩니다. 합성 데이터는 "스키마와 규칙으로부터 만들어져야" 합니다. 실제 데이터를 전달하는 것은 편리해 보일지 몰라도 데이터 역류 사고의 원인이 됩니다.
테스트 데이터에서 또 하나 중요한 것은 **결정성 (Determinism)**입니다. LLM은 그대로 두면 매번 조금씩 다른 것을 반환하기 때문에, 그대로 두면 테스트가 "어제는 통과했는데 오늘은 실패하는" 식으로 불안정해집니다.
대책은 간단합니다. "생성은 단 한 번만 수행한다. 결과는 파일로 고정(스냅샷)하고, 테스트는 그 파일을 읽는다"는 설계로 가는 것입니다. 생성할 때도 seed (난수 시드)를 고정할 수 있는 부분은 최대한 고정합니다.
# freeze_seed.py — 생성 결과를 JSON으로 고정하고, 이후에는 그것을 사용한다
import json, random, hashlib
from pathlib import Path
...
이렇게 하면 테스트 측은 load_frozen()을 읽기만 하면 됩니다. 매번 LLM을 호출하지 않으므로 빠르고, 결정론적(Deterministic)이며, API 비용도 들지 않습니다. LLM을 사용하는 것은 "데이터셋을 업데이트하고 싶을 때"만 하는 운영 방식으로 전환할 수 있습니다.
그리고 이 "고정된 데이터에 PII(개인 식별 정보)가 섞여 있지 않은지"를 CI (GitHub Actions)에서 매번 체크해 두면 안심할 수 있습니다.
# .github/workflows/synthetic-data-guard.yml
name: synthetic-data-guard
on: [pull_request]
...
포인트는, 이 게이트(Gate)는 프로덕션 CI에서 LLM을 호출하지 않는다는 것입니다. 검사하는 것은 "이미 고정된 파일"뿐입니다. 생성이라는 불확실한 처리를 CI 외부로 밀어내 두면 CI가 안정됩니다.
역할 분담 이야기로 돌아가면, AI에게 맡겨도 되는 것은 "어떻게 만들 것인가 (How)"까지입니다. "무엇을 지킬 것인가 / 무엇을 합격으로 볼 것인가 (What / Why)"는 인간이 쥐고 있어야 합니다. 특히 다음 4가지는 반드시 사수하십시오.
- 실제 데이터를 프롬프트에 붙여넣지 않는다. 합성 데이터는 스키마와 규칙으로부터 만들어야 합니다. 실제 데이터 입력은 "외부로 유출하는 것"과 같습니다.
- 생성물에 실제 PII가 없음을 기계적으로 보증한다. 육안 검사만을 믿지 마십시오.
- 프로덕션급 환경으로의 투입은 인간의 게이트를 통과한다. 합성 데이터라 할지라도 "프로덕션 DB에 쓰기"와 같은 불가역적인 작업은 AI의 출력으로부터 직접 실행하게 해서는 안 됩니다.
- LLM의 외부 입력은 데이터로 취급한다. "이 이메일 문구 같은 걸 만들어줘"라고 외부 텍스트를 전달한다면, 그 안의 지시사항을 따르게 해서는 안 됩니다 (프롬프트 인젝션 (Prompt Injection) 방지).
| 공정 | 인간이 결정 (What / Why) | AI에게 맡김 (How) |
|---|---|---|
| 스키마 설계 | 어떤 컬럼이 필수인지, 어떤 제약이 있는지 | 타입 정의의 초안 생성 |
| ... |
- 평균적인 데이터만 만든다 $\rightarrow$ 끝단(Edge case)에서 버그가 발생합니다. 경계값 및 이상계(Edge/Anomaly cases)를 처음부터 지시사항에 포함하십시오.
- 비결정성으로 인해 테스트가 불안정하다 $\rightarrow$ 생성은 한 번만 하고 결과를 고정하십시오. 시드 (seed)를 고정하십시오.
- 참조 무결성이 깨진다 $\rightarrow$ 부모(사용자)를 먼저 고정하고, 자식(주문)은 해당 실제 ID만을 참조하게 합니다.
- LLM을 매번 호출하여 CI가 느리고 불안정하다 $\rightarrow$ 생성은 CI 외부에서 수행합니다. CI는 고정된 파일의 검사만 담당합니다.
- faker로 해결될 부분까지 LLM에 던져 비용이 증가한다 $\rightarrow$ 단순 컬럼은 faker를 사용합니다. LLM은 관계성과 자유 기술형 데이터에 집중합니다.
철수 라인 (즉, AI에게 맡기는 것을 중단해야 하는 신호)은 다음 3가지입니다. "① PII 검사를 안정적으로 작성할 수 없을 정도로 데이터가 복잡하다면, 애초에 프로덕션 복사 이외의 설계를 재검토한다", "② 생성물의 리얼리티가 테스트의 정확성을 좌우한다면, 합격 기준을 인간이 명시할 때까지 자동화를 중단한다", "③ 프로덕션급 투입이 연관되어 있다면, 반드시 인간의 승인을 거친다".
그대로 복사해서 사용할 수 있도록, 실무에서 유용한 프롬프트 3개를 남겨둡니다.
① 스키마로부터 최소 생성
다음 Pydantic 스키마를 엄격히 따르는 가공의 데이터를 10건 만들어주세요.
실제 개인 정보는 사용하지 말고, 이메일은 example.com만 사용하세요.
값은 각 제약 조건(타입, 범위, 열거형)을 반드시 충족해야 합니다.
...
② 경계값 및 이상계 트리아지 (Triage)
이 스키마에 대해 테스트에서 다뤄야 할 경계값 및 이상계를 도출하고,
"목표 경계", "왜 버그가 발생하기 쉬운가", "샘플 값"을 포함한 표로 만들어주세요.
마지막으로, 각각의 샘플 레코드를 JSON으로 출력해 주세요.
...
③ PII 검사 규칙 초안
이 데이터 구조에 대해 실제 PII가 혼입되지 않았는지 검사하는
Python 함수의 초안을 작성해 주세요.
검사 대상(이메일/전화번호/성명/주소 등)과 허용할 더미 값을 명시하고,
...
모두 공통적인 점은, 마지막 판단을 인간에게 돌려주고 있다는 것입니다. AI에게는 "초안"과 "후보 제시"를 맡기고, 결정하는 것은 자신입니다. 이 거리감이 속도와 안전 사이의 딱 적절한 균형이라고 생각합니다.
길어졌습니다만, 오늘 이야기를 한마디로 요약하자면——테스트 데이터를 프로덕션에서 복사하는 것을 그만두고, 스키마와 규칙으로부터 "제로(Zero)에서 만드는 것", 그것뿐입니다.
이것은 화려하지는 않지만, 서서히 효과를 발휘합니다. 운영 환경에 PII (개인정보)가 흩어져 있는 환경이 줄어들고, 심야에 "그 검증 환경, 개인정보가 들어있었잖아..."라며 얼굴이 창백해지는 횟수가 줄어듭니다. 테스트는 결정론적 (Deterministic)이 되고, CI (지속적 통합)는 안정됩니다. 그리고 무엇보다, 경계값(Boundary value)까지 정교하게 만들어진 테스트 데이터는 한 번 만들어두면 자산으로서 쌓여갑니다. 구매한 것이 아니라, 우리 스스로 설계한 "도구"가 되는 것이죠. 이것은 바로 코드와 데이터를 자산으로 바꾸어 나가는 (Code as Capital) 발상 그 자체입니다.
오늘 아주 조금만 움직여서, example.com의 더미 사용자 10명을 만들고, PII 검사를 한 번 돌려둡니다. 그것만으로도 내일의 내가 "고마워요"라고 말해줄 것입니다. 누군가를 비난하기 위한 축이 아니라, 배려의 축으로서 미래의 자신에게 작은 선물을 놓아두는 느낌입니다.
우선 오늘 첫걸음을 떼기 위한 4단계입니다.
- 가장 PII가 위험한 테이블(대개 사용자 관련)의 스키마를 Pydantic으로 한 장 작성한다
- Structured Outputs를 사용하여 가공의 데이터를 10건 생성해 본다
assert_no_pii()를 한 번 실행하여example.com이외의 것을 걸러낸다- 결과를 JSON으로 고정하고, 테스트에서 이를 읽어온다
여기까지 완료했다면, 이미 "운영 데이터 복사 졸업"의 입구에 서 있는 것입니다. 함께 안심하고 테스트할 수 있는 개발 플로우를 키워 나갑시다.
생성형 AI 활용 엔지니어 & 세 아이의 아빠. AI × 개발의 실천적 지식을 매일 발신하고 있습니다. → X: https://x.com/akira_papa_AI
- OpenAI Structured Outputs (공식 가이드): https://platform.openai.com/docs/guides/structured-outputs
- Pydantic 공식 문서: https://docs.pydantic.dev/
- Faker 공식 문서: https://faker.readthedocs.io/
- SDV (Synthetic Data Vault): https://sdv.dev/
- 합성 데이터 생성 도구 비교 (Tonic.ai 블로그): https://www.tonic.ai/blog/synthetic-data-generation-tools
- GDPR (EU 일반 데이터 보호 규칙): https://gdpr.eu/
- HIPAA (미국 의료정보 보호법 / HHS): https://www.hhs.gov/hipaa/
※ 본문의 OpenAI SDK 메서드명·모델 스냅샷 명칭은 2026년 시점의 작성 방식입니다. 최신 모델명·API 사양은 각 사의 공식 문서에서 확인해 주세요 (코드는 구문 체크 완료 · API 통신은 동작 미확인).
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기