입문편의 정규식 점검은 주민번호·전화번호처럼 모양이 정해진 개인정보를 잡습니다. 하지만 정규식이 원리상 못 잡는 것이 있습니다 — "김철수 소방관이 마포구 성산동 현장에서"의 이름과 주소입니다. 모양이 없고 문맥으로만 사람이라는 걸 알 수 있기 때문입니다. 통화 몇 건이라면 입문편의 3단 점검 마지막 단계에서 사람이 지우면 되지만, 수천 건을 AI 학습·분석에 쓰려면 사람이 일일이 지울 수 없습니다.
왜 정규식만으로는 부족한가
| 개인정보 | 모양(패턴) | 정규식 | 문맥 탐지(NER) |
|---|---|---|---|
| 주민번호·전화·차량번호 | 있음 | ✅ 강함 | 불필요 |
| 이름 | 없음 | ❌ 못 잡음 | ✅ 문맥으로 |
| 상세주소·지명 | 약함 | △ 일부 | ✅ 문맥으로 |
| 병원명·직책 조합 | 없음 | ❌ | △ 커스텀 필요 |
핵심은 두 방식을 겹치는 것입니다. 정규식은 패턴 있는 것을 확실히, NER(개체명 인식)은 문맥 있는 것을. Presidio는 이 둘을 한 엔진에서 묶어 줍니다.
1. Presidio — 탐지와 익명화 분리
Presidio는 탐지(Analyzer) 와 익명화(Anonymizer) 를 나눕니다. 먼저 무엇이 어디 있는지 찾고, 그 다음 어떻게 가릴지 정합니다. 전 과정 로컬에서 돕니다 — 신고 전사와 같은 원칙입니다.
from presidio_analyzer import AnalyzerEngine from presidio_anonymizer import AnonymizerEngine from presidio_anonymizer.entities import OperatorConfig analyzer = AnalyzerEngine() anonymizer = AnonymizerEngine() text = "신고자 김철수님, 마포구 성산동, 연락처 010-1234-5678" # 1) 탐지 — 한국어 NER 모델을 spaCy/HuggingFace로 구성(아래 주의 참고) found = analyzer.analyze(text=text, language="ko", entities=["PERSON", "LOCATION", "PHONE_NUMBER"]) # 2) 익명화 — 개체 유형별로 가리는 방식 지정 result = anonymizer.anonymize( text=text, analyzer_results=found, operators={ "PERSON": OperatorConfig("replace", {"new_value": "<이름>"}), "LOCATION": OperatorConfig("replace", {"new_value": "<지역>"}), "PHONE_NUMBER": OperatorConfig("mask", {"masking_char": "*", "chars_to_mask": 8, "from_end": True}), }) print(result.text) # 신고자 <이름>님, <지역>, 연락처 010-********
NlpEngine으로 연결하며, 붙인 모델의 성능이 곧 이름·지명 탐지의 성능입니다 — 3단계에서 반드시 재현율을 재세요.2. 소방 특화 커스텀 인식기
일반 NER는 소방 고유 표현을 모릅니다 — 사건번호, 출동번호, 관서 내부 코드 같은 것입니다. 입문편의 정규식 자산을 Presidio 커스텀 인식기로 옮겨, 기존 엔진에 합칩니다.
from presidio_analyzer import Pattern, PatternRecognizer # 입문편 BLOCK_PATTERNS를 그대로 재사용 — 사건·출동번호도 추가 rrn = PatternRecognizer( supported_entity="KR_RRN", # 주민등록번호 patterns=[Pattern("rrn", r"\b\d{6}[-\s]?[1-8]\d{6}\b", 0.9)]) case_no = PatternRecognizer( supported_entity="KR_CASE_NO", # 사건번호(관서 서식에 맞춰 조정) patterns=[Pattern("case", r"\b\d{4}-\d{5,6}\b", 0.6)]) analyzer.registry.add_recognizer(rrn) analyzer.registry.add_recognizer(case_no)
정규식 인식기는 문맥이 없어도 확실히 잡고, NER 인식기는 문맥으로 잡습니다. 둘을 한 레지스트리에 넣으면 탐지 한 번으로 양쪽을 받습니다.
3. 재현율 검증 — 놓친 것이 진짜 위험
비식별에서 최악의 실수는 놓치는 것(재현율 실패)입니다. 가려야 할 이름을 하나 흘리면 그 한 건으로 개인정보가 노출됩니다. 반대로 안 가려도 될 걸 가리는 것(정밀도)은 데이터가 조금 뭉개질 뿐 안전은 지켜집니다. 그래서 재현율을 정밀도보다 우선해 관리합니다.
합성 신고 데이터로 골든셋을 만듭니다 — 문장마다 "가려야 할 개체 목록"을 사람이 표시합니다. 파이프라인을 돌려 얼마나 놓쳤는지 셉니다.
def recall(gold_spans, found_spans): """가려야 할 것 중 실제로 잡은 비율. 놓친 개수를 함께 본다.""" hit = sum(1 for g in gold_spans if any(overlap(g, f) for f in found_spans)) missed = [g for g in gold_spans if not any(overlap(g, f) for f in found_spans)] return hit / max(len(gold_spans), 1), missed r, missed = recall(gold, found) print(f"재현율 {r:.1%}, 놓친 개체 {len(missed)}건") for m in missed: # 놓친 것은 전부 사람이 보고 커스텀 인식기를 보강 print(" 놓침:", m)
4. 일관된 가명화 — 재식별과 활용의 균형
이름을 전부 <이름>으로 바꾸면 안전하지만, "같은 사람이 여러 번 나온다"는 정보가 사라져 분석이 어려워집니다. 같은 원본은 같은 가명으로 치환하면(가명화) 안전과 활용을 함께 얻습니다 — "김철수→인물A"가 문서 전체에서 일관되게 유지됩니다.
단, 이때 원본↔가명 대응표(entity_mapping)는 그 자체가 최고 민감 자료입니다. 이 표만 있으면 재식별이 되기 때문입니다. 대응표는 비식별 데이터와 절대 같은 곳에 두지 않고, 분석에 재식별이 필요 없다면 만들지 않는 것이 가장 안전합니다.
운영 루틴 한 장
| 언제 | 무엇을 | 통과 기준 |
|---|---|---|
| 파이프라인·모델 변경 시 | 골든셋 재현율 회귀 | 이전 대비 하락 없음 |
| 매 배치 | 비식별 결과 표본 재검수 | 놓친 개체 0 (나오면 인식기 보강) |
| 놓침 발견 시 | 커스텀 인식기·골든셋 보강 | 같은 유형 재발 방지 |
| 상시 | 원본↔가명 대응표 격리 | 비식별 데이터와 분리 저장·최소 권한 |
사람 검토 체크리스트
- [ ] 정규식(패턴)과 NER(문맥) 인식기를 함께 써서 이름·주소까지 탐지했습니다. - [ ] 한국어 NER 모델을 붙였고, 그 모델의 재현율을 골든셋으로 확인했습니다. - [ ] 재현율을 정밀도보다 우선해 관리하고, 놓친 개체를 사람이 검토했습니다. - [ ] 소방 특화 개체(사건·출동번호 등)를 커스텀 인식기로 보강했습니다. - [ ] 가명화 대응표를 비식별 데이터와 분리 저장하고 권한을 최소화했습니다. - [ ] 연습·검증은 합성 데이터로만 했습니다.