고객 데이터를 Claude나 GPT API에 넣기 전에 개인정보를 자동으로 지워야 합니다. 클라우드로 보내지 않고, 로컬에서, 코드 3줄로 해결합니다.
[핵심 요약]
→ 정체: 텍스트 안의 개인정보를 자동 탐지/마스킹하는 오픈소스 모델
→ 크기: 1.5B 파라미터 (50M 활성) — 노트북에서 실행 가능
→ 라이센스: Apache 2.0 — 상업 이용 무료
→ 컨텍스트: 128K 토큰 — 긴 문서/이메일 스레드 한 번에 처리
→ 탐지 범주: 이름, 주소, 이메일, 전화번호, URL, 날짜, 계좌번호, 비밀번호/API 키
→ 성능: PII-Masking-300k F1 97.43%
→ 핵심: 데이터가 외부 서버로 나가지 않음
왜 필요한가
LLM API를 쓰다 보면 개인정보가 섞인 데이터를 넣게 되는 상황이 자주 생깁니다.
흔한 위험 상황:
→ 고객 상담 내역을 GPT에 넣고 요약 요청
→ 사내 계약서를 Claude에 넣고 분석 요청
→ 의료 기록을 LLM으로 처리
→ 이메일 스레드를 AI로 분류
→ 코드 리뷰 요청 시 API 키가 섞인 코드 전송
결과:
→ 이름, 이메일, 전화번호가 OpenAI/Anthropic 서버로 전송
→ GDPR, 개인정보보호법 위반 가능
→ 금융/의료/법무: 규정상 외부 전송 자체가 불가
기존 해결 방법의 한계:
→ 정규식 기반 필터: 이메일/전화번호 패턴은 잡지만 문맥 의존 정보 놓침
→ 예시: "김철수 대리에게 연락해"에서 "김철수"는 정규식으로 못 잡음
→ 예시: "비밀번호는 Tistory@2026!"에서 비밀번호 탐지 불가
→ Privacy Filter: 문맥 파악해서 탐지 → 위 두 케이스 모두 마스킹
실전 1 — 설치 및 기본 사용
# 설치
pip install git+https://github.com/openai/privacy-filter.git
# 또는 Hugging Face에서 직접
pip install transformers torch
가장 빠른 방법은 transformers pipeline입니다.
from transformers import pipeline
# 모델 로드 (첫 실행 시 자동 다운로드 ~3GB)
classifier = pipeline(
task="token-classification",
model="openai/privacy-filter",
device=0 # GPU 사용, CPU는 device=-1
)
# 기본 사용
text = """
안녕하세요, 저는 김철수입니다.
연락처는 010-1234-5678이고
이메일은 kim@example.com입니다.
계좌번호는 110-123-456789입니다.
"""
result = classifier(text)
print(result)
# 출력 예시
# [
# {'entity': 'S-private_person', 'word': '김철수', 'score': 0.98},
# {'entity': 'S-private_phone', 'word': '010-1234-5678', 'score': 0.99},
# {'entity': 'S-private_email', 'word': 'kim@example.com', 'score': 0.99},
# {'entity': 'S-account_number', 'word': '110-123-456789', 'score': 0.97},
# ]
[탐지 가능한 8가지 카테고리]
→ private_person: 개인 이름
→ private_address: 개인 주소
→ private_email: 개인 이메일
→ private_phone: 개인 전화번호
→ private_url: 개인 URL
→ private_date: 개인 날짜 (생년월일 등)
→ account_number: 계좌번호, 카드번호
→ secret: 비밀번호, API 키, 토큰
실전 2 — 자동 마스킹 함수 만들기
탐지 결과를 받아서 실제로 텍스트를 마스킹하는 함수입니다.
from transformers import pipeline
import re
# 모델 로드
pii_detector = pipeline(
task="token-classification",
model="openai/privacy-filter",
aggregation_strategy="simple", # 연속된 토큰을 하나의 엔티티로 합침
device=0
)
def mask_pii(text: str, mask_char: str = "[MASKED]") -> str:
"""
텍스트에서 개인정보를 탐지하고 마스킹 처리
Args:
text: 마스킹할 원본 텍스트
mask_char: 마스킹 문자열 (기본값: [MASKED])
Returns:
마스킹된 텍스트
"""
if not text.strip():
return text
# PII 탐지
entities = pii_detector(text)
if not entities:
return text
# 뒤에서부터 교체 (인덱스 밀림 방지)
entities_sorted = sorted(
entities,
key=lambda x: x['start'],
reverse=True
)
masked_text = text
for entity in entities_sorted:
start = entity['start']
end = entity['end']
label = entity['entity_group']
score = entity['score']
# 신뢰도 0.85 이상만 마스킹
if score >= 0.85:
masked_text = (
masked_text[:start] +
f"[{label.upper()}]" +
masked_text[end:]
)
return masked_text
# 사용 예시
texts = [
"김철수(010-9876-5432)에게 연락 바랍니다.",
"API_KEY=sk-abc123def456 로 인증해주세요.",
"서울시 강남구 테헤란로 123번지 거주자 이영희",
"생년월일 1990년 3월 15일, 계좌 123-456-789012",
]
for text in texts:
masked = mask_pii(text)
print(f"원본: {text}")
print(f"마스킹: {masked}")
print()
# 출력:
# 원본: 김철수(010-9876-5432)에게 연락 바랍니다.
# 마스킹: [PRIVATE_PERSON]([PRIVATE_PHONE])에게 연락 바랍니다.
#
# 원본: API_KEY=sk-abc123def456 로 인증해주세요.
# 마스킹: API_KEY=[SECRET] 로 인증해주세요.
#
# 원본: 서울시 강남구 테헤란로 123번지 거주자 이영희
# 마스킹: [PRIVATE_ADDRESS] 거주자 [PRIVATE_PERSON]
#
# 원본: 생년월일 1990년 3월 15일, 계좌 123-456-789012
# 마스킹: 생년월일 [PRIVATE_DATE], 계좌 [ACCOUNT_NUMBER]
실전 3 — LLM API 호출 전 자동 마스킹 파이프라인
실제로 Claude나 GPT API 호출 전에 끼워넣는 패턴입니다.
import anthropic
from transformers import pipeline
# Privacy Filter 초기화
pii_filter = pipeline(
task="token-classification",
model="openai/privacy-filter",
aggregation_strategy="simple",
device=0
)
def safe_llm_call(user_input: str, system_prompt: str = "") -> str:
"""
개인정보를 마스킹한 후 LLM API 호출
"""
# 1단계: PII 마스킹
masked_input = mask_pii(user_input)
# 마스킹된 내용이 있으면 로그
if masked_input != user_input:
print(f"[PII 마스킹 완료] 원본 길이: {len(user_input)}, 마스킹 후: {len(masked_input)}")
# 2단계: 마스킹된 텍스트로 LLM 호출
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=system_prompt,
messages=[
{"role": "user", "content": masked_input}
]
)
return response.content[0].text
# 실제 사용 예시
customer_complaint = """
고객명: 박민준 (010-5555-1234)
이메일: park.minjun@company.co.kr
계좌: 우리은행 1002-345-678901
불만 내용: 지난 3월 15일 결제한 금액이 환불되지 않고 있습니다.
빠른 처리 부탁드립니다.
"""
response = safe_llm_call(
user_input=customer_complaint,
system_prompt="고객 불만 내용을 분석하고 처리 방법을 안내해주세요."
)
print("LLM 응답:", response)
# Claude에 실제로 전달되는 텍스트:
# 고객명: [PRIVATE_PERSON] ([PRIVATE_PHONE])
# 이메일: [PRIVATE_EMAIL]
# 계좌: 우리은행 [ACCOUNT_NUMBER]
# 불만 내용: 지난 [PRIVATE_DATE] 결제한 금액이 환불되지 않고 있습니다.
[파이프라인 동작 원리]
→ 사용자 입력 → Privacy Filter → 마스킹된 텍스트 → LLM API
→ 개인정보는 로컬에서만 처리, 외부 서버로 나가지 않음
→ LLM 응답에는 마스킹된 플레이스홀더가 그대로 유지
→ 필요시 로컬 DB에서 마스킹 맵을 보관해 역매핑 가능
실전 4 — 배치 처리 (대량 문서 마스킹)
수천 건의 문서를 한 번에 처리할 때 쓰는 패턴입니다.
import json
from pathlib import Path
from transformers import pipeline
from tqdm import tqdm
pii_filter = pipeline(
task="token-classification",
model="openai/privacy-filter",
aggregation_strategy="simple",
device=0,
batch_size=16 # 배치 크기 설정
)
def batch_mask_documents(
documents: list[dict],
text_field: str = "content",
output_path: str = "masked_documents.json"
) -> list[dict]:
"""
대량 문서 배치 마스킹
Args:
documents: 문서 리스트 [{"id": ..., "content": ...}, ...]
text_field: 마스킹할 텍스트 필드명
output_path: 결과 저장 경로
"""
masked_docs = []
texts = [doc[text_field] for doc in documents]
# 배치로 PII 탐지
print(f"총 {len(texts)}개 문서 처리 중...")
for i, (doc, text) in enumerate(tqdm(zip(documents, texts))):
masked_text = mask_pii(text)
masked_doc = doc.copy()
masked_doc[text_field] = masked_text
masked_doc["pii_masked"] = (masked_text != text)
masked_docs.append(masked_doc)
# 결과 저장
with open(output_path, "w", encoding="utf-8") as f:
json.dump(masked_docs, f, ensure_ascii=False, indent=2)
masked_count = sum(1 for d in masked_docs if d["pii_masked"])
print(f"완료: {masked_count}/{len(documents)}개 문서에서 PII 탐지됨")
return masked_docs
# 사용 예시
documents = [
{"id": 1, "content": "홍길동(hong@test.com)의 주문 내역입니다."},
{"id": 2, "content": "일반적인 제품 설명 텍스트입니다."},
{"id": 3, "content": "신용카드 번호: 4532-1234-5678-9012"},
]
result = batch_mask_documents(documents)
[배치 처리 포인트]
→ batch_size=16: GPU 메모리에 맞게 조정 (VRAM 4GB → 8, 8GB → 16, 24GB → 32)
→ 128K 컨텍스트: 긴 문서도 잘라서 처리할 필요 없음
→ CPU 모드: device="cpu" — 느리지만 GPU 없어도 동작
→ tqdm: 대량 처리 시 진행 상황 확인
실전 5 — CLI로 빠르게 쓰기
코드 없이 터미널에서 바로 쓰는 방법입니다.
# CLI 설치
pip install git+https://github.com/openai/privacy-filter.git
# 텍스트 직접 입력
echo "김철수 010-1234-5678 kim@test.com" | opf mask
# 파일 마스킹
opf mask --input document.txt --output masked_document.txt
# JSON 파일 배치 처리
opf mask --input data.jsonl --field content --output masked_data.jsonl
# CPU 모드 (GPU 없을 때)
opf mask --device cpu --input document.txt
# 신뢰도 임계값 조정 (기본 0.85)
# 높을수록 정확하지만 탐지율 낮아짐
# 낮을수록 탐지율 높지만 오탐 늘어남
opf mask --threshold 0.90 --input document.txt
[CLI 활용 시나리오]
→ 데이터 파이프라인: cat raw.txt | opf mask > clean.txt
→ 전처리 스크립트에 통합: 학습 데이터 정제 전 자동 실행
→ 로그 정제: 서버 로그에서 이메일/IP 마스킹
→ CI/CD: PR 전 코드베이스 스캔 (API 키 유출 방지)
기존 도구 대비 차이
# 정규식 기반 방식 (기존)
import re
def regex_mask(text):
# 이메일 패턴
text = re.sub(r'\b[\w.-]+@[\w.-]+\.\w+\b', '[EMAIL]', text)
# 전화번호 패턴
text = re.sub(r'\d{3}-\d{3,4}-\d{4}', '[PHONE]', text)
# 이름은? → 불가능
# 문맥 의존 정보는? → 불가능
return text
# Privacy Filter 방식 (신규)
# 문맥 파악 → 이름, 문맥 의존 날짜, 숨겨진 API 키까지 탐지
[정규식 vs Privacy Filter]
정규식 Privacy Filter
이름 탐지: ❌ 불가 ✅ 가능
이메일: ✅ 패턴 매칭 ✅ 문맥 기반
전화번호: ✅ 패턴 매칭 ✅ 문맥 기반
API 키/비번: △ 일부만 ✅ 다양한 형식
문맥 의존: ❌ ✅ 128K 컨텍스트
처리 속도: ✅ 매우 빠름 △ 모델 추론 필요
GPU 필요: ❌ △ CPU 가능, GPU 권장
오픈소스: ✅ ✅ Apache 2.0
마무리
✅ 써야 할 때
→ LLM API에 고객 데이터/사내 문서를 넣는 모든 서비스
→ GDPR, 개인정보보호법 준수가 필요한 환경
→ 금융/의료/법무 등 데이터 외부 전송 불가 규제 환경
→ 학습 데이터 정제 (개인정보 포함된 크롤링 데이터)
→ 로그 파일에서 개인정보 자동 제거
→ 이름 같은 문맥 의존 정보까지 잡아야 할 때
❌ 안 써도 될 때
→ 개인정보가 전혀 없는 순수 기술 문서 처리
→ 정규식으로 충분한 단순 이메일/전화번호 패턴만 처리
→ 초저지연(< 10ms) 실시간 처리가 필요할 때 (정규식이 빠름)
→ 한국어 특수 명칭에서 정확도가 중요할 때 (한국어 파인튜닝 권장)
관련 글
OpenAI Agents SDK 대규모 업데이트 — Claude Code Routines 나온 지 3일 만에 맞불
2026년 4월 14일, Anthropic이 Claude Code Routines를 출시했어요. 3일 뒤인 4월 16일, OpenAI가 Agents SDK를 대규모 업데이트했어요.타이밍이 우연이 아닌 것 같은 이유:Claude Code Routines: 노트북 꺼도 클라우드에
cell-devlog.tistory.com
https://cell-devlog.tistory.com/98
AI 코딩 툴 보안 실전 — Claude Code 소스 유출 이후 달라진 공격 지형
2026년 3월 31일, Claude Code 소스코드가 npm에 실수로 노출됐어요.유출 규모:→ 51만 2천 줄 TypeScript→ 1,906개 파일→ 24시간 만에 GitHub 포크 41,500개공격자들의 반응 속도:→ 유출 24시간 이내: 악성 "leak
cell-devlog.tistory.com
'LLM' 카테고리의 다른 글
| Kimi K2.6 완전 분석 — 오픈소스가 GPT-5.4를 이기고 Claude 비용의 10%로 돌아간다 (0) | 2026.04.28 |
|---|---|
| Microsoft MAI 모델 3종 완전 분석 — OpenAI 없이 만든 음성·이미지 API 실전 가이드 (0) | 2026.04.27 |
| 오픈소스 코딩 모델 3파전 — Qwen3.6-27B vs Gemma 4 31B vs GLM-5.1 (0) | 2026.04.24 |
| Qwen3.6-27B로 로컬 코딩 에이전트 만들기 — Aider, Continue.dev, Cursor, Qwen Code 완전 연동 가이드 (0) | 2026.04.24 |
| Qwen3.6-27B vs 35B-A3B — Dense vs MoE (0) | 2026.04.24 |