© 2026 DEV BAK - 기술블로그. All rights reserved.
DEV BAK - 기술블로그
전체AIBackendClaudeDevOpsOpenClawOpenSourcefrontend
DevOps

Presidio + spaCy로 한국어 PII 탐지 파이프라인 구축하기

커스텀 NLP 엔진 연동부터 이름·주소 인식까지

AI/ML · 데이터 엔지니어링

사전 지식: Python 3.8+ 기본 문법, pip 패키지 설치 경험, NLP에 대한 가벼운 관심이면 충분합니다.


목차

  • 핵심 개념
    • Presidio의 구조
    • spaCy 한국어 모델
    • Recognizer의 종류
  • 실전 적용
  • 실무에서 가장 흔한 실수
  • 장단점 분석
  • 마치며

처음 Presidio를 한국어 텍스트에 돌려봤을 때의 당혹감을 아직도 기억합니다. "김철수에게 연락주세요"를 넣었더니 "김철수에게"를 통째로 하나의 토큰으로 잡고, 주민등록번호는 아예 못 찾는 거였죠. Microsoft가 만든 오픈소스 PII 탐지 프레임워크라길래 기대하고 붙여봤는데, 영어에서는 꽤 잘 돌아가던 것이 한국어 앞에서는 맥을 못 추더라고요.

2025년 개인정보보호위원회가 「생성형 AI 개발·활용을 위한 개인정보 처리 안내서」를 발표하면서, AI 학습 데이터 내 PII 필터링은 이제 선택이 아니라 의무가 되었습니다. 그래서 직접 삽질하면서 알아낸 걸 정리해봤습니다. spaCy 한국어 모델을 NLP 엔진으로 교체하고, 한국 특유의 PII 형식에 맞는 커스텀 인식기를 직접 구현하면, 이 글을 끝까지 따라왔을 때 동작하는 한국어 PII 탐지 파이프라인 코드 전체를 손에 넣을 수 있습니다.


핵심 개념

Presidio의 구조 — 왜 "커스텀 NLP 엔진"이 필요한가

Presidio는 크게 두 개의 엔진으로 나뉩니다. 텍스트에서 PII를 찾아내는 AnalyzerEngine과, 찾아낸 PII를 마스킹·치환·암호화하는 AnonymizerEngine입니다. 이 중 AnalyzerEngine 내부에는 토큰화, 형태소 분석, 개체명 인식(NER)을 수행하는 NlpEngine이 자리잡고 있는데, 기본값이 영어 spaCy 모델(en_core_web_lg)입니다.

NER(Named Entity Recognition, 개체명 인식)이란? 텍스트에서 사람 이름, 장소, 기관명 같은 고유명사를 자동으로 찾아내 분류하는 NLP 기술입니다. "김철수가 서울에서 근무한다"라는 문장에서 "김철수"→인물, "서울"→장소로 태깅하는 것이 NER의 역할입니다.

css
[텍스트 입력]
    ↓
[NlpEngine] → 토큰화 + NER (spaCy/Stanza/Transformers)
    ↓
[Recognizer Registry] → 정규식 매칭 + 컨텍스트 키워드 + NER 결과 종합
    ↓
[RecognizerResult] → 엔티티 타입, 위치, 신뢰도 점수
    ↓
[AnonymizerEngine] → 마스킹 / 치환 / 암호화

핵심은 NlpEngine이 플러그인 방식이라는 점입니다. NlpEngineProvider에 설정 딕셔너리나 YAML 파일을 넘겨주면, spaCy 한국어 모델이든 Hugging Face Transformer든 원하는 모델로 교체할 수 있습니다. 그리고 Presidio의 PII 인식은 NER 하나에만 의존하지 않고, NER + 정규식 패턴 + 체크섬 검증 + 컨텍스트 키워드를 다층으로 결합합니다. 이 다층 구조 덕분에 한국어처럼 NER 하나만으로는 커버가 안 되는 환경에서도 여러 겹의 안전망을 칠 수 있습니다.

NlpEngine이란? Presidio 내부에서 텍스트의 토큰화·형태소 분석·개체명 인식을 담당하는 추상 계층입니다. spaCy, Stanza, Hugging Face Transformers 세 가지 백엔드를 공식 지원하며, 어떤 백엔드를 쓰든 Presidio의 나머지 파이프라인은 동일하게 동작합니다.


spaCy 한국어 모델 — 무엇을 할 수 있고 어디가 한계인가

spaCy v3.3부터 도입된 한국어 파이프라인(ko_core_news_sm/md/lg)은 tok2vec, tagger, morphologizer, parser, lemmatizer, senter, ner 컴포넌트를 포함하고 있습니다. NER은 KLUE 데이터셋(한국어 자연어 이해 능력을 평가하기 위한 벤치마크 데이터셋으로, NER·문장 유사도·감성 분석 등 다양한 태스크를 포함합니다)으로 학습되어 인물(PER), 장소(LOC), 기관(ORG) 등을 인식합니다.

실무에서 주목할 점이 두 가지 있는데, 먼저 md와 lg 모델은 floret 벡터를 사용합니다.

floret 벡터란? fastText의 서브워드 임베딩을 Bloom filter 기반 해시 테이블로 압축한 벡터 기술입니다. 모델 크기를 크게 늘리지 않으면서도 미등록어에 대한 벡터 표현이 가능해, 조사·어미 변화가 풍부한 교착어에 특히 효과적입니다.

"김철수에게"처럼 조사가 붙은 형태도 벡터 표현이 가능하다는 뜻이죠. 다른 하나는 토큰화 방식인데, UD Korean Kaist 기반의 공백+구두점 분절을 사용하기 때문에 mecab-ko 없이도 동작합니다. 배포 환경에서 mecab-ko 설치가 번거로웠던 분들에게는 꽤 반가운 소식입니다.

하지만 한계도 분명합니다:

난제 구체적 상황 영향
조사 결합 "김철수에게", "서울시에서" 개체 경계가 모호해져 NER 정확도 하락
한국 특유 PII 형식 주민등록번호 900101-1234567, 휴대전화 010-1234-5678 기본 내장 인식기 미지원
주소 체계의 복잡성 "서울특별시 강남구 테헤란로 123 OO빌딩 4층" 계층 구조 + 신구 주소 혼용 + 약어
짧은 이름 한국인 이름 2~4글자 일반 명사와의 구분이 어려움

Presidio Recognizer의 종류 — 어떤 걸 써야 할까

한국어 PII 탐지를 구현하려면 Presidio의 인식기(Recognizer) 체계를 이해하는 것이 중요합니다:

인식기 타입 적합한 PII 특징
PatternRecognizer 주민등록번호, 전화번호, 이메일 정규식 + 컨텍스트 키워드 기반, 구현이 간단
EntityRecognizer 인물명, 기관명, 주소 NER 결과를 활용하거나 커스텀 로직 구현
RemoteRecognizer 복잡한 PII 외부 API 호출로 인식 위임

실무에서 자주 맞닥뜨리는 상황인데, 주민등록번호나 전화번호처럼 형식이 정해진 PII는 PatternRecognizer로 충분하고, 이름이나 주소처럼 문맥 의존적인 PII는 EntityRecognizer를 상속받아 NER 결과에 추가 검증 로직을 얹는 방식이 효과적입니다.


실전 적용

예시 1: spaCy 한국어 모델로 Presidio 엔진 세팅하기

가장 먼저 해야 할 일은 Presidio의 NLP 엔진을 한국어 모델로 교체하는 것입니다.

bash
pip install presidio-analyzer presidio-anonymizer
python -m spacy download ko_core_news_lg
python
from presidio_analyzer import AnalyzerEngine
from presidio_analyzer.nlp_engine import NlpEngineProvider
 
configuration = {
    "nlp_engine_name": "spacy",
    "models": [
        {"lang_code": "ko", "model_name": "ko_core_news_lg"}
    ]
}
 
provider = NlpEngineProvider(nlp_configuration=configuration)
nlp_engine = provider.create_engine()
 
analyzer = AnalyzerEngine(
    nlp_engine=nlp_engine,
    supported_languages=["ko"]
)
 
results = analyzer.analyze(
    text="김철수의 전화번호는 010-1234-5678이고 주민등록번호는 900101-1234567입니다.",
    language="ko"
)
 
for r in results:
    print(f"{r.entity_type}: score={r.score}, start={r.start}, end={r.end}")
코드 요소 역할
nlp_engine_name: "spacy" NLP 백엔드로 spaCy를 지정
lang_code: "ko" 한국어 언어 코드 등록
model_name: "ko_core_news_lg" KLUE 기반 학습된 대형 한국어 모델 사용 (공식 문서 기준 약 550MB)
supported_languages=["ko"] AnalyzerEngine이 한국어 입력을 처리하도록 설정

이 상태로 돌려보면 이런 결과가 나옵니다:

PERSON: score=0.85, start=0, end=4

"김철수"는 PERSON으로 잡히지만, 주민등록번호와 전화번호는 결과에 아예 나타나지 않습니다. 기본 내장 인식기가 영어권 PII(SSN, US phone number 등)만 지원하기 때문이죠. 여기서부터 커스텀 인식기가 필요해집니다.


예시 2: 한국 주민등록번호·전화번호 커스텀 PatternRecognizer

형식이 정해진 PII는 정규식 기반 PatternRecognizer로 깔끔하게 처리할 수 있습니다.

python
from presidio_analyzer import Pattern, PatternRecognizer
 
# 주민등록번호 인식기
kr_rrn_pattern = Pattern(
    name="kr_rrn_pattern",
    regex=r"\b(\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])-?([1-4])\d{6}\b",
    score=0.85
)
 
kr_rrn_recognizer = PatternRecognizer(
    supported_entity="KR_RRN",
    supported_language="ko",
    patterns=[kr_rrn_pattern],
    context=["주민등록번호", "주민번호", "생년월일"]
)
 
# 휴대전화번호 인식기
kr_phone_pattern = Pattern(
    name="kr_phone_pattern",
    regex=r"\b(01[016789])-?(\d{3,4})-?(\d{4})\b",
    score=0.7
)
 
kr_phone_recognizer = PatternRecognizer(
    supported_entity="KR_PHONE_NUMBER",
    supported_language="ko",
    patterns=[kr_phone_pattern],
    context=["전화번호", "휴대폰", "연락처", "핸드폰"]
)
 
# ⚠️ 예시 1에서 생성한 analyzer 인스턴스를 그대로 사용합니다
analyzer.registry.add_recognizer(kr_rrn_recognizer)
analyzer.registry.add_recognizer(kr_phone_recognizer)
파라미터 설명
regex 주민등록번호는 생년월일 유효성까지 검증하는 패턴 사용
score 기본 신뢰도 점수. 주민등록번호는 형식 자체가 고유하므로 0.85, 전화번호는 일반 숫자열과 혼동 가능하므로 0.7
context 주변에 이 키워드가 있으면 점수가 상향 조정됨. 한국어 키워드를 충분히 넣는 것이 포인트

⚠️ 주민등록번호 정규식의 알려진 한계: 위 패턴의 [1-4]는 내국인 주민등록번호만 커버합니다. 외국인등록번호는 뒷자리가 5~8로 시작하므로, 외국인등록번호까지 탐지하려면 [1-8]로 확장이 필요합니다. 또한 \b(워드 바운더리)가 한글 문맥에서 기대와 다르게 동작할 수 있으므로, 프로덕션에서는 (?<=\s|^)와 (?=\s|$) 같은 명시적 경계 조건을 사용하는 편이 안전합니다.

이제 커스텀 인식기를 등록한 후 같은 텍스트를 다시 분석하면:

python
results = analyzer.analyze(
    text="김철수의 전화번호는 010-1234-5678이고 주민등록번호는 900101-1234567입니다.",
    language="ko"
)
 
for r in results:
    print(f"{r.entity_type}: score={r.score}, start={r.start}, end={r.end}")
sql
PERSON:          score=0.85, start=0,  end=4
KR_PHONE_NUMBER: score=0.7,  start=12, end=25
KR_RRN:          score=0.85, start=36, end=50

전화번호와 주민등록번호가 모두 잡히는 것을 확인할 수 있습니다. 여기서 context 파라미터가 은근히 강력한데, "전화번호는 010-1234-5678입니다"라는 문장에서 "전화번호"라는 키워드가 주변에 있으면 기본 점수 0.7에서 상향 조정됩니다. 저도 처음엔 이걸 대충 넣었다가, 컨텍스트 키워드를 꼼꼼히 채운 후 정확도가 눈에 띄게 좋아진 경험이 있습니다.


예시 3: NER 기반 한국어 이름 인식 강화

개인적으로 가장 삽질을 많이 한 부분입니다. spaCy 한국어 모델의 NER이 PER 레이블로 인물명을 잡아주긴 하지만, "이수"가 사람 이름인지 "이수(理數)"라는 일반 명사인지 구분하기가 쉽지 않습니다. 처음에는 성씨 사전 없이 NER 결과만 그대로 썼더니 '이수'를 전부 사람 이름으로 잡아서, 데이터 절반이 마스킹되는 참사가 벌어졌습니다. 결국 NER 결과에 한국 성씨 사전 기반 검증을 얹는 방식으로 신뢰도를 끌어올렸습니다.

python
from presidio_analyzer import EntityRecognizer, RecognizerResult
 
KOREAN_SURNAMES = {
    "김", "이", "박", "최", "정", "강", "조", "윤", "장", "임",
    "한", "오", "서", "신", "권", "황", "안", "송", "류", "전",
    "홍", "고", "문", "양", "손", "배", "백", "허", "유", "남",
    "심", "노", "하", "곽", "성", "차", "주", "우", "구", "민",
}
 
class KoreanNameRecognizer(EntityRecognizer):
    def load(self):
        pass
 
    def analyze(self, text, entities, nlp_artifacts=None):
        results = []
        if not nlp_artifacts:
            return results
 
        for ent in nlp_artifacts.entities:
            if ent.label_ not in ("PER", "PERSON"):
                continue
 
            # ⚠️ rstrip은 문자 집합 기반이므로 "이만"→"" 같은 오작동 위험이 있습니다.
            # 프로덕션에서는 반드시 형태소 분석기(mecab-ko, kiwi 등) 사용을 권장합니다.
            name = ent.text.rstrip("이가을를에게의와과도만")
 
            if len(name) < 2:
                continue
 
            score = 0.7
            if name[0] in KOREAN_SURNAMES:
                score = 0.9
 
            results.append(RecognizerResult(
                entity_type="KR_PERSON",
                start=ent.start_char,
                end=ent.start_char + len(name),
                score=score,
            ))
        return results
 
# ⚠️ 예시 1에서 생성한 analyzer 인스턴스를 그대로 사용합니다
kr_name_recognizer = KoreanNameRecognizer(
    supported_entities=["KR_PERSON"],
    supported_language="ko",
)
analyzer.registry.add_recognizer(kr_name_recognizer)
처리 단계 설명
NER 필터링 spaCy NER 결과 중 PER/PERSON 레이블만 추출
조사 제거 rstrip으로 뒤에 붙은 조사를 제거하여 순수 이름 추출
성씨 검증 첫 글자가 한국 성씨 사전에 있으면 점수 0.7 → 0.9로 상향
길이 검증 한국인 이름은 최소 2글자이므로 그 미만은 제외

rstrip 코드가 위험한 이유: rstrip("이가을를에게의와과도만")은 문자열이 아니라 문자 집합을 기준으로 strip합니다. 즉 "이만"이라는 이름에서 "이"와 "만"을 모두 떼어내 빈 문자열이 됩니다. 위 코드에서는 len(name) < 2 체크로 빈 문자열은 걸러지지만, "이만수"가 "수"로 잘리는 등 예상치 못한 결과가 나올 수 있습니다. 프로덕션 환경에서는 반드시 형태소 분석기(mecab-ko, kiwipiepy 등)를 사용하여 조사를 정확히 분리하는 것을 권장합니다.

한 가지 더 주의할 점은, ent.start_char + len(name)으로 end 위치를 계산하는 부분입니다. 조사를 strip한 후의 name 길이와 원문에서의 위치가 일치하지 않는 엣지 케이스가 있을 수 있으므로, 실무에서는 아래와 같은 테스트를 꼭 돌려보시면 좋습니다:

python
test_cases = [
    "김철수에게 연락했다",
    "이만수의 기록",
    "박지성과 손흥민이 만났다",
]
 
for text in test_cases:
    results = analyzer.analyze(text=text, language="ko")
    for r in results:
        detected = text[r.start:r.end]
        print(f"원문: '{text}' → 탐지: '{detected}' ({r.entity_type}, {r.score})")

예시 4: 다국어 구성 (영어 + 한국어)

영어와 한국어가 섞인 텍스트를 처리해야 하는 경우 — 클릭하여 펼치기

YAML 설정 파일로 다국어 엔진을 깔끔하게 구성할 수 있습니다.

yaml
# languages-config.yml
nlp_engine_name: spacy
models:
  - lang_code: ko
    model_name: ko_core_news_lg
  - lang_code: en
    model_name: en_core_web_lg
python
provider = NlpEngineProvider(conf_file="./languages-config.yml")
nlp_engine = provider.create_engine()
 
analyzer = AnalyzerEngine(
    nlp_engine=nlp_engine,
    supported_languages=["ko", "en"]
)
 
ko_results = analyzer.analyze(
    text="고객 김철수(010-9876-5432)의 주민등록번호는 850315-1234567입니다.",
    language="ko"
)
 
en_results = analyzer.analyze(
    text="Contact John at john@example.com",
    language="en"
)

하나의 Presidio 인스턴스로 한국어·영어 PII를 모두 처리할 수 있어 편리합니다. 다만 PatternRecognizer는 하나의 언어만 지원하는 제약(Issue #1606)이 있어서, 한국어용과 영어용 인식기를 각각 만들어 등록해야 합니다.


예시 5: RAG 파이프라인에 PII 필터 삽입하기

여기까지 오면 기본적인 한국어 PII 탐지가 동작하는데, 실제 RAG에 붙이려니 또 다른 고민이 생겼습니다. 마스킹 결과를 어떤 포맷으로 내보낼지, 탐지 실패 시 로그를 어떻게 남길지 같은 문제였죠. 최근에 가장 수요가 많은 패턴인 RAG 전처리 통합 예시로 마무리해보겠습니다.

python
import logging
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
 
logger = logging.getLogger(__name__)
anonymizer = AnonymizerEngine()
 
def mask_pii_for_rag(text: str) -> str:
    """RAG 파이프라인 전단에서 PII를 마스킹하는 전처리 함수"""
    results = analyzer.analyze(text=text, language="ko")
    logger.info(f"PII 탐지 결과: {len(results)}건 발견")
 
    if not results:
        return text
 
    anonymized = anonymizer.anonymize(
        text=text,
        analyzer_results=results,
        operators={
            "KR_RRN": OperatorConfig("replace", {"new_value": "[주민번호]"}),
            "KR_PHONE_NUMBER": OperatorConfig("replace", {"new_value": "[전화번호]"}),
            "KR_PERSON": OperatorConfig("replace", {"new_value": "[이름]"}),
            "DEFAULT": OperatorConfig("replace", {"new_value": "[개인정보]"}),
        }
    )
    return anonymized.text
 
# 사용 예
raw_text = "고객 김철수(010-1234-5678)의 주민등록번호는 900101-1234567입니다."
safe_text = mask_pii_for_rag(raw_text)
print(safe_text)
# 결과: "고객 [이름]([전화번호])의 주민등록번호는 [주민번호]입니다."

이렇게 하면 LLM 컨텍스트에 주민등록번호나 전화번호 같은 민감 정보가 유입되는 것을 차단할 수 있습니다. 개인정보보호위원회 안내서에서 요구하는 "AI 학습 데이터 내 개인정보 비식별화"를 기술적으로 이행하는 가장 직접적인 방법이기도 합니다.


실무에서 가장 흔한 실수

이 부분은 따로 빼서 강조하고 싶을 만큼 중요합니다. 제가 겪었거나 주변에서 반복적으로 봤던 실수 세 가지입니다.

1. context 키워드를 빈 리스트로 두는 것

정규식 패턴만 넣고 컨텍스트 키워드를 비워두면 점수 상향 조정이 일어나지 않아 탐지율이 크게 떨어집니다. "주민등록번호", "주민번호", "생년월일" 등 실제 문서에서 쓰이는 다양한 표현을 충분히 넣어두면 체감 정확도가 확 달라집니다.

2. 조사 후처리를 빠뜨리는 것

NER이 "김철수에게"를 통째로 PER로 잡았을 때 그대로 Presidio에 넘기면, 익명화 시 "에게"까지 마스킹되어 "[이름] 연락했다"가 되어버립니다. 문맥이 깨지는 건 물론이고, 후속 NLP 처리에도 악영향을 미칩니다.

3. 테스트 데이터 없이 배포하는 것

실제 개인정보를 사용할 수 없으므로, 가상의 이름·주민등록번호·전화번호 조합으로 테스트셋을 구축하고 precision/recall을 측정하는 과정은 건너뛸 수 없습니다.

Precision vs Recall이란? Precision(정밀도)은 "PII라고 판단한 것 중 실제 PII의 비율", Recall(재현율)은 "실제 PII 중 탐지에 성공한 비율"입니다. PII 탐지에서는 놓치는 것(false negative)이 오탐(false positive)보다 위험하므로, 재현율을 먼저 확보한 후 정밀도를 튜닝하는 전략이 일반적입니다.


장단점 분석

장점

항목 내용
모듈식 아키텍처 NLP 엔진·인식기·익명화기가 분리되어 있어 한국어 모델만 교체하거나 인식기만 추가할 수 있습니다
다층 탐지 전략 NER + 정규식 + 컨텍스트 키워드를 결합하여 단일 방식 대비 높은 재현율 확보가 가능합니다
Transformer 통합 TransformersNlpEngine으로 KLUE-BERT, KoBERT 등을 직접 연결할 수 있습니다. spaCy NER 대비 이름·주소 같은 문맥 의존적 엔티티에서 체감 정확도가 크게 올라가는데, 이 부분은 다음 글에서 상세히 다루겠습니다
운영 환경 친화적 Docker 이미지, REST API 서버 모드를 제공하여 마이크로서비스에 쉽게 통합할 수 있습니다
활발한 생태계 Microsoft 주도 하에 꾸준히 업데이트되고, LLM 연동 등 차세대 기능 논의가 진행 중입니다

단점 및 주의사항

항목 내용 대응 방안
한국어 기본 지원 부재 영어 외 언어는 "bring your own model" 방식으로, 커스텀 인식기 구현 초기 비용이 큽니다 이 글의 코드를 기반으로 주민등록번호·전화번호·이름 인식기부터 구축하고 점진적으로 확장하는 것이 현실적입니다
NER 정확도 한계 구어체, SNS, 고객 상담 로그 등 실무 텍스트에서 정확도가 떨어질 수 있습니다 KLUE-BERT 등 Transformer 모델로 교체하거나, 도메인 특화 파인튜닝을 고려해 볼 수 있습니다
조사 처리 문제 "김철수에게" 같은 형태에서 개체 경계 인식 오류가 빈번합니다 NER 결과 후처리 단계에서 형태소 분석기 기반 조사 분리 로직이 필수입니다
모델 크기 ko_core_news_lg가 공식 문서 기준 약 550MB로 메모리·로딩 시간 부담이 있습니다 경량 환경에서는 ko_core_news_sm으로 시작 후 필요에 따라 업그레이드하는 방식이 있습니다
한국식 주소 인식 계층 구조 + 신구 주소 혼용 + 약어로 정규식만으로는 한계가 있습니다 NER(LOC) + 주소 키워드 사전 + 정규식을 결합한 복합 인식기가 필요합니다

마치며

Presidio의 모듈식 구조 덕분에 spaCy 한국어 모델 교체와 커스텀 인식기 추가만으로도, 영어 전용이었던 PII 탐지 파이프라인을 한국어 환경에 맞게 확장할 수 있습니다. 물론 완벽하지는 않고, 특히 조사 처리나 주소 인식에서는 아직 손이 많이 가는 게 사실입니다. 하지만 규제가 강화되고 있는 지금, "완벽해질 때까지 기다리기"보다 "쓸 만한 수준에서 시작해 점진적으로 개선하기"가 훨씬 현실적인 전략이라고 생각합니다. 제 경험상 주민등록번호·전화번호 recall 95% 이상, 인물명 recall 80% 이상이면 1차 배포로 충분하고, 이후 실서비스 로그를 보면서 튜닝해나가는 게 효율적이었습니다.

지금 바로 시작해볼 수 있는 3단계:

  1. 환경 세팅 — pip install presidio-analyzer presidio-anonymizer && python -m spacy download ko_core_news_lg로 의존성을 설치하고, 예시 1 코드로 한국어 NLP 엔진이 정상 동작하는지 확인해 볼 수 있습니다.

  2. 커스텀 인식기 등록 — 예시 2의 주민등록번호·전화번호 PatternRecognizer와 예시 3의 KoreanNameRecognizer를 프로젝트에 추가하고, 실제 업무 텍스트(비식별화된)로 탐지 결과를 확인해보시면 좋습니다.

  3. 파이프라인 통합 — 예시 5의 mask_pii_for_rag 패턴을 참고하여 RAG 파이프라인이나 데이터 전처리 워크플로에 PII 필터를 삽입해 볼 수 있습니다. Presidio는 REST API 모드도 지원하므로, 공식 Docker 이미지를 활용하면 마이크로서비스로 운영하는 것도 가능합니다.


다음 글: Transformer 모델로 한 단계 더 — TransformersNlpEngine에 KLUE-BERT를 연결하여 spaCy NER의 한계를 넘는 한국어 PII 탐지 고도화 방법을 다루겠습니다.


참고 자료

필수 참고 — 이 글의 코드를 따라하거나 확장할 때 가장 먼저 볼 문서들입니다:

  • Microsoft Presidio GitHub Repository — 프레임워크 소스코드와 Docker 이미지
  • Presidio 커스텀 NLP 엔진 설정 가이드 | Microsoft — NLP 엔진 교체 방법 상세 설명
  • Presidio 언어 추가 튜토리얼 | Microsoft — 비영어권 언어 지원을 위한 공식 튜토리얼
  • spaCy 한국어 모델 공식 문서 | Explosion — 모델 크기·성능·컴포넌트 상세 정보
  • KLUE Benchmark — 한국어 NER 학습에 사용된 벤치마크 데이터셋

심화 참고 — 특정 이슈를 깊이 파고들거나 대안을 탐색할 때 유용합니다:

  • Presidio Analyzer 공식 문서 | Microsoft
  • Presidio 다국어 지원 문서 | Microsoft
  • Presidio spaCy/Stanza NLP 엔진 문서 | Microsoft
  • Presidio Transformer NLP 엔진 문서 | Microsoft
  • spaCy 한국어 NER 조사 포함 이슈 (Issue #13705) | GitHub
  • spaCy 한국어 파이프라인 피드백 토론 (Discussion #10624) | GitHub
  • PatternRecognizer 다국어 지원 이슈 (Issue #1606) | GitHub
  • LlamaIndex Presidio PII 마스킹 가이드 | LlamaIndex
  • 생성형 AI 개인정보 처리 안내서 | 개인정보보호위원회
  • Kaggle: PII Microsoft Presidio KR 노트북
  • mcp-pii-tools — 한국어 PII 도구 | GitHub
공유하기

목차

핵심 개념Presidio의 구조 — 왜 "커스텀 NLP 엔진"이 필요한가spaCy 한국어 모델 — 무엇을 할 수 있고 어디가 한계인가Presidio Recognizer의 종류 — 어떤 걸 써야 할까실전 적용예시 1: spaCy 한국어 모델로 Presidio 엔진 세팅하기예시 2: 한국 주민등록번호·전화번호 커스텀 PatternRecognizer예시 3: NER 기반 한국어 이름 인식 강화예시 4: 다국어 구성 (영어 + 한국어)예시 5: RAG 파이프라인에 PII 필터 삽입하기실무에서 가장 흔한 실수장단점 분석장점단점 및 주의사항마치며참고 자료

추천 포스트

Presidio + KLUE-BERT로 한국어 PII 탐지 고도화하기 — spaCy NER의 한계를 넘는 실전 가이드
DevOps

Presidio + KLUE-BERT로 한국어 PII 탐지 고도화하기 — spaCy NER의 한계를 넘는 실전 가이드

목차 - 핵심 개념 - 사전 준비 - 실전 적용 - spaCy 단독 vs. Transformer 교체 — Before/After 비교 - 장단점 분석 - 실무에서 가장 흔한 실수 - 마치며 - 참고 자료 솔직히 고백하면, 저도 처음에는 spaCy의 모델만으로 한국어...

2026년 04월 20일읽는 데 30분
Presidio 커스텀 Recognizer로 한국형 PII 잡기 — 정규표현식 + 체크섬 + 컨텍스트 3중 검증 구현
DevOps

Presidio 커스텀 Recognizer로 한국형 PII 잡기 — 정규표현식 + 체크섬 + 컨텍스트 3중 검증 구현

솔직히 고백하면, 처음 PII 탐지를 맡았을 때 "정규표현식 몇 개 짜면 되겠지"라고 가볍게 생각했다가 크게 데인 적이 있습니다. 주민등록번호 패턴으로 을 돌렸더니, 전화번호부터 주문번호까지 온갖 숫자 조합이 줄줄이 걸려 나왔습니다. 고객 데이터 8만 건에 정규표현식만 돌렸을 때 fal...

2026년 04월 21일읽는 데 29분
PII 마스킹 누락, 배포 전에 자동으로 잡아내기
DevOps

PII 마스킹 누락, 배포 전에 자동으로 잡아내기

Presidio와 CI 파이프라인으로 구축하는 마스킹 누락 탐지 시스템 솔직히 고백하면, 저도 한때 마스킹 규칙을 스프레드시트로 관리했습니다. DB에 컬럼 하나 추가할 때마다 "마스킹 규칙 업데이트했나?" 하고 슬랙에 물어보는 게 프로세스의 전부였죠. 그러다 어느 날 스테이징 환경...

2026년 04월 20일읽는 데 30분
마스킹된 프로덕션 DB로 E2E 테스트 돌리기 — Playwright + Neon 브랜칭 파이프라인 구축기
DevOps

마스킹된 프로덕션 DB로 E2E 테스트 돌리기 — Playwright + Neon 브랜칭 파이프라인 구축기

"테스트 데이터가 너무 깨끗해서 버그를 못 잡았다"는 말, 한 번쯤 들어보셨을 겁니다. 저도 시드 스크립트로 만든 예쁜 데이터 위에서 테스트를 통과시켜놓고, 프로덕션에서 터지는 걸 몇 번이나 겪었는지 모릅니다. 이메일이 200자인 사용자, 주소 필드에 개행 문자가 들어간 레코드, 외래 ...

2026년 04월 20일읽는 데 28분
Preview 환경에 프로덕션 데이터를 안전하게 제공하는 방법
DevOps

Preview 환경에 프로덕션 데이터를 안전하게 제공하는 방법

Neon 마스킹 브랜치 vs 커스텀 익명화 스크립트, 두 달간 병행한 실전 비교 "스테이징에서는 잘 되는데 프로덕션에서 터졌어요." 개발자라면 한 번쯤은 들어봤을 이 문장의 원인 중 상당수는 테스트 데이터가 현실을 반영하지 못해서 생긴다. 10건짜리 시드 데이터로는 N+1 쿼리 문...

2026년 04월 20일읽는 데 26분
Preview 환경에서 공유 DB를 안전하게 쓰는 법 — PR별 스키마 격리와 시드 데이터 자동화
DevOps

Preview 환경에서 공유 DB를 안전하게 쓰는 법 — PR별 스키마 격리와 시드 데이터 자동화

솔직히 고백하자면, 저도 Preview 환경에서 데이터베이스 문제로 하루를 통째로 날린 적이 있습니다. 동료가 올린 PR의 마이그레이션이 제 Preview 환경의 테이블을 날려버린 거죠. "누가 내 테이블 DROP 했어?"라는 슬랙 메시지를 보내본 경험, 다들 한 번쯤은 있지 않나요? ...

2026년 04월 20일읽는 데 21분