본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 05. 12:38

일본어 프롬프트의 개인정보를 익명화하여 Claude로 보내는 챗봇을 구축한 이야기

요약

Microsoft Presidio를 활용하여 일본어 프롬프트 내 개인정보(PII)를 자동으로 검출하고 익명화하여 Claude API로 전달하는 챗봇 구축 사례를 소개합니다. 영어 중심인 Presidio를 일본어 환경에 맞게 커스터마이징하는 기술적 핵심을 다룹니다.

핵심 포인트

  • Microsoft Presidio를 이용한 일본어 PII 검출 및 마스킹 구현
  • spaCy와 SudachiPy를 활용한 일본어 NLP 환경 구축
  • 개인정보를 플레이스홀더로 치환하여 LLM 보안 강화
  • Streamlit을 이용한 사용자 친화적 채팅 UI 구현

1. 서론

안녕하세요!

주식회사 우구이스 솔루션즈(うぐいすソリューションズ)에서 엔지니어로 근무하고 있는 Kouki입니다.

ChatGPT나 Claude와 같은 LLM(대규모 언어 모델)을 업무에 도입할 때, 많은 현장에서 가장 먼저 맞닥뜨리는 문제가 "개인정보(PII: Personally Identifiable Information)를 외부 API로 보내도 되는가"라는 문제입니다. 클레임 대응 초안 작성, 견적 메일 답장, 지원자 정보 요약——이러한 유스케이스에서는 성명, 전화번호, 주소, 마이넘버(My Number)와 같은 생(raw) 개인정보가 입력 텍스트에 자연스럽게 섞여 들어갑니다.

그래서 본 기사에서는 Microsoft Presidio를 사용하여 일본어 텍스트에서 개인정보를 자동 검출 및 마스킹(Masking)하고, 마스킹된 텍스트만을 Claude API로 보내는 챗봇을 만든 이야기를 쓰고자 합니다. 특히 Presidio는 영어 전제로 만들어진 부분이 많아, 일본어 대응에는 상응하는 커스터마이징이 필요했습니다. 그 핵심 포인트를 중심으로 소개하겠습니다.

소스 코드는 아래 리포지토리(Repository)에 공개되어 있으므로, 기사를 어느 정도 읽으신 후에는 자신의 환경에서 Claude 등의 AI에 해석시켜 보는 것이 훨씬 빠르게 이해하는 방법일 것입니다.

2. 기술 스택

분류라이브러리 / 도구버전역할
언어Python3.13구현 언어
PII 검출·익명화Microsoft Presidio (presidio-analyzer / presidio-anonymizer)2.2.x개인정보 검출 및 익명화의 기반
NLPspaCy3.8.x토큰화(Tokenization)·개체명 인식(NER)
일본어 모델ja_core_news_lg3.8.0spaCy의 일본어 학습済み 모델
일본어 토크나이저SudachiPy-spaCy 일본어 모델이 내부적으로 사용
LLMAnthropic SDK (anthropic)0.104.xClaude API 클라이언트
UIStreamlit1.57.x채팅 UI

3. 만든 것

전체적인 흐름은 심플합니다.

사용자가 채팅에 입력한다 (개인정보를 포함해도 됨)

Presidio가 입력을 분석하여, 검출된 PII를 플레이스홀더(Placeholder)로 치환한다

치환 전

고객 지원팀에 접수된 다음 클레임 메일에 대해, 사과와 향후 대응 방침을 포함한 정중한 답장 문구를 작성해 주세요. 또한 답장은 외부 고객용이므로 경어를 사용하여 부드러운 표현을 유의해 주세요.
「안녕하십니까. 야마다 타로(山田太郎)라고 합니다. 요전번에 귀사의 온라인 숍에서 주문한 상품(주문 번호 A12345-678)이 예정된 배송일보다 3일이 지났는데도 도착하지 않았습니다. 회의에서 사용할 예정이었기에 매우 곤란한 상황입니다. 조속히 배송 상황을 확인하신 후 연락 부탁드립니다. 낮에는 휴대전화 090-1234-5678로 연락 주시고, 연결되지 않을 경우 이메일 taro.yamada@example.com으로 부탁드립니다. 청구지 주소는 도쿄도 신주쿠구 니시신주쿠 2-8-1입니다. 번거로우시겠지만 잘 부탁드립니다.」

치환 후

고객 지원팀에 접수된 다음 클레임 메일에 대해, 사과와 향후 대응 방침을 포함한 정중한 답장 문구를 작성해 주세요. 또한 답장은 외부 고객용이므로 경어를 사용하여 부드러운 표현을 유의해 주세요.
「안녕하십니까. <성명_1>이라고 합니다. 요전번에 귀사의 온라인 숍에서 주문한 상품(주문 번호 A12345-678)이 예정된 배송일보다 <일시_1>이 지났는데도 도착하지 않았습니다. 회의에서 사용할 예정이었기에 매우 곤란한 상황입니다. 조속히 배송 상황을 확인하신 후 연락 부탁드립니다. 낮에는 휴대전화 <전화번호_1>로 연락 주시고, 연결되지 않을 경우 이메일 <이메일 주소_1>으로 부탁드립니다. 청구지 주소는 <주소_1>입니다. 번거로우시겠지만 잘 부탁드립니다.」

전송 전에 "무엇을 마스킹할 것인가"를 사용자에게 제시하여 승인을 얻는다

승인되면 마스킹된 텍스트만을 Claude API로 전송한다

Claude의 응답에 포함된 플레이스홀더를, 사용자 승인 시의 원래 값으로 복호화한다

핵심은 Claude API에 전달되는 것은 반드시 마스킹된 텍스트뿐이라는 점입니다. 원문(생 개인정보)은 로컬 화면 표시용으로만 사용되며, 외부로 유출되지 않습니다.

4. Presidio란 무엇인가 (아키텍처)

Microsoft Presidio는 텍스트나 이미지에서 PII(개인 식별 정보)를 탐지 및 익명화하기 위한 OSS(오픈 소스 소프트웨어) 프레임워크입니다. 크게 두 가지 엔진으로 나뉩니다.

4.1. presidio-analyzer (탐지)

AnalyzerEngine이 텍스트를 분석하여 RecognizerResult(엔티티 유형·시작/종료 위치·신뢰도 점수) 리스트를 반환합니다. Analyzer는 내부에 다음 요소들을 가집니다.

  • NlpEngine: spaCy나 Stanza와 같은 NLP(자연어 처리) 라이브러리의 래퍼(Wrapper). 토큰화(Tokenization) 및 개체명 인식(NER)을 담당합니다.
  • RecognizerRegistry: 개별 인식기(Recognizer)의 집합입니다.
  • Recognizer: 실제로 PII를 찾는 부품입니다. PatternRecognizer(정규 표현식 + 문맥어 기반)와 NER 결과를 받아들이는 SpacyRecognizer 등이 있습니다.

탐지 흐름은 "NlpEngine이 텍스트를 NLP 처리 → 각 Recognizer가 그 결과나 정규 표현식을 사용하여 PII 후보를 도출 → 점수 임계값으로 필터링"하는 방식입니다.

4.2. presidio-anonymizer (익명화)

AnonymizerEngine이 탐지 결과를 받아 replace(고정 문자열로 치환)·mask(**** 처리)·hash·redact(삭제)·encrypt(암호화)와 같은 **연산자(Operator)**로 익명화합니다. 탐지와 익명화가 느슨하게 결합(Loosely Coupled)되어 있는 것이 Presidio 설계의 강점이며, 탐지기만 교체하거나 익명화 방식만 변경하는 것이 용이합니다.

from presidio_analyzer import AnalyzerEngine, RecognizerRegistry
from presidio_analyzer.nlp_engine import NlpEngineProvider
nlp_engine = NlpEngineProvider(nlp_configuration={
...

5. 왜 일본어는 어려운가

Presidio의 표준 인식기는 영어를 전제로 하고 있어, 일본어에 그대로 적용하면 몇 가지 함정에 빠지게 됩니다. 구현 중에 마주쳤던 사례들을 나열합니다.

5.1. 함정 1: \b가 작동하지 않음

표준 EmailRecognizerCreditCardRecognizer는 정규 표현식에 단어 경계(Word Boundary)인 \b를 사용합니다. 그런데 Python의 정규 표현식에서는 일본어(한자·가나)도 "단어 문자"로 취급되기 때문에, PII가 일본어에 직접 인접해 있으면 \b가 성립되지 않아 탐지에 실패합니다.

카드 번호는 4111-1111-1111-1111입니다 → 표준 인식기에서는 탐지되지 않음

"4"의 앞부분이 "는"이라는 단어 문자이므로, \b(단어와 비단어의 경계)가 세워지지 않는 것입니다. 이는 영어권에서는 발생하지 않지만, 일본어에서는 문장 속에 PII가 포함되는 것이 일반적이므로 치명적입니다.

5.2. 함정 2: 숫자열의 의미가 문맥에 의존함

전화번호·우편번호·마이넘버·운전면허증 번호·신용카드는 모두 "숫자의 나열"입니다. 자릿수나 구분 기호만으로는 판별하기 어렵습니다. 045-678-9012의 일부인 678-9012는 우편번호처럼 보이기도 합니다.

5.3. 함정 3: spaCy의 NER이 일반어를 오탐지함

ja_core_news_lg의 NER은 우수하지만, 비즈니스 문장의 일반어("고객", "담당", "출석자", "당사" 등)를 인명·조직명·지명으로 잘못 판정하는 경우가 있습니다. 또한 숫자열을 높은 점수로 DATE_TIME이라고 판정하는 경향이 있습니다.

6. 일본어 대응을 위한 커스터마이징

이러한 문제들에 대해 다음과 같이 조치를 취했습니다.

\b를 대체하기

6.1. \b 대신 **전방 탐색·후방 탐색(Lookaround)**을 사용하여 "앞뒤가 숫자·영문자가 아님"을 표현하는 독자적인 PatternRecognizer를 준비합니다. 이메일 주소라면 다음과 같습니다.

Pattern(
name="jp_email",
regex=(r"(?<![A-Za-z0-9._%+\-])"
...

(?<!...)는 "직전이 이 문자 클래스가 아니다"라는 의미로, 일본어가 인접해 있어도 매칭됩니다. 신용카드도 마찬가지로 (?<![0-90-9])...(?![0-90-9])를 사용하여 전후를 숫자가 아닌 것으로 한정하고 있습니다.

반각·전각 숫자([0-90-9])나 각종 하이픈(-‐–—ー- 등)을 허용하는 문자 클래스를 정의해 두는 것도 일본어만의 특징입니다.

6.2. 일본의 정형 PII를 정규 표현식으로 검출

전화번호·우편번호·마이넘버(My Number)·주소·여권·운전면허증·은행 계좌를 각각 PatternRecognizer로 정의했습니다. 예를 들어 전화번호는 휴대폰·유선전화·프리다이얼을 별도의 패턴으로 가집니다.

# 휴대폰: 070/080/090-XXXX-XXXX
Pattern(name="jp_mobile", regex=r"0[789]0-?\d{4}-?\d{4}", score=0.7)
# 유선전화: 지역번호-국번-가입자번호
...

유선전화의 스코어(0.65)를 우편번호(0.6)보다 높게 설정한 이유는, 045-678-9012와 같은 번호에서 "전화번호 전체"가 "우편번호 부분 일치"보다 우선하도록 하기 위해서입니다. 주소는 47개 도도부현(都道府県) 명을 앵커(Anchor)로 삼아, 시구정촌(市区町村)부터 초메(丁目)·번지·호까지 대략적으로 포착합니다. 완전한 구문 분석(Parsing)은 정규 표현식으로 어렵기 때문에, 과부족을 허용하는 과감한 설계로 구성했습니다.

6.3. 문맥어 (context) 및 스코어 설계

PatternRecognizer에는 context 인자를 통해 **문맥어 (context words)**를 전달할 수 있습니다. 검출 위치 근처에 문맥어가 있으면 Presidio가 스코어를 가산해 주는 메커니즘입니다. 은행 계좌나 마이넘버와 같은 "단순 숫자열"은 기준 스코어를 낮게(0.3) 설정하고, "계좌", "송금", "마이넘버" 등의 단어가 근처에 있을 때만 임계값을 넘어 검출되도록 했습니다.

PatternRecognizer(
supported_entity="JP_BANK_ACCOUNT",
patterns=[Pattern(name="jp_bank_account", regex=r"...\d{7,8}...", score=0.3)],
...

여기서 일본어 특유의 함정이 있었습니다. 문맥어는 형태소 단위로 대조되기 때문에, "우편번호(郵便番号)"는 "우편(郵便)", "번호(番号)"로 분리될 수 있습니다. 따라서 context=["郵便番号", "郵便", "番号"]와 같이 분리된 단어도 함께 등록해 두어야 합니다.

6.4. 경칭을 통한 성명 보강

NER이 모두 잡아내지 못하는 성명을 보완하기 위해, 경칭(Honorific) 직전을 성명 후보로 하는 인식기를 추가했습니다. 전방 탐색(Lookahead)을 사용하여 경칭 자체는 치환 대상에 포함하지 않습니다.

name = r"[一-龥々ぁ-んァ-ヶ]{2,10}"
honorific = r"(?=(?:さん|様|氏|くん|先生|部長|課長|社長))"
Pattern(name="jp_person_honorific", regex=name + honorific, score=0.45)

"다나카 씨(田中さん)"에서 "다나카(田中)"만 성명으로 검출할 수 있습니다.

6.5. 중복 검출 해결

동일한 위치에 여러 엔티티(Entity)가 걸리는 경우가 있습니다 (예: spaCy가 150-0043LOCATION으로, 우편번호 인식기가 JP_POSTAL_CODE로 검출하는 경우 등). 그래서 중복 구간을 다음 우선순위에 따라 하나로 압축하는 처리를 직접 작성했습니다.

  • 스코어가 높은 것을 우선
  • 점수가 같다면 전용 라벨(JP_*)을 범용 라벨(LOCATION/DATE_TIME)보다 우선
  • 그것도 같다면 더 긴 구간을 우선

이를 통해 spaCy가 숫자열을 DATE_TIME으로 오판하더라도, 전용 전화번호·우편번호 검출이 승리합니다. 압축된 집합을 그대로 치환에 사용함으로써, 화면의 검출 목록과 마스킹 후 텍스트의 라벨이 반드시 일치하게 됩니다.

6.6. denylist로 과검출 억제

"고객(お客)”, "담당(担当)”, "당사(弊社)”, "회의실(会議室)” 등 비즈니스 문장에서 빈번하게 등장하는 단어를 NER_DENYLIST

에 나열하고, 이들이 PERSON/ORGANIZATION/LOCATION으로 판정되더라도 제외합니다. 개인정보 보호에서는 "놓치는 것보다 과도하게 마스킹하는 것"이 안전하므로 임계값(Threshold)은 낮게(0.4) 설정하되, 명확한 일반어만 핀포인트로 제거하는 방식을 취했습니다.

7. 가역 마스킹 (송신 전 확인 및 복호화)

당초에는 Presidio 표준의 AnonymizerEngine을 사용하여 모든 항목을 <氏名>(성명)과 같은 고정 문자열로 치환했습니다. 하지만 이렇게 하면 여러 사람이 등장했을 때 "어느 <氏名>이 누구인지" 구분할 수 없어 응답을 원래대로 되돌릴 수 없습니다.

그래서 플레이스홀더(Placeholder)를 <氏名_1>, <氏名_2>와 같이 고유한 일련번호가 붙은 형태로 만들고, 플레이스홀더 → 원래 값의 매핑(Mapping)을 유지하도록 변경했습니다. 치환은 masker.py 내에서 수동으로 구현하였으며, 동일한 값에는 동일한 토큰을 재사용하고, 다른 값에는 일련번호를 증가시킵니다. 대화를 넘나들며 번호를 계속 부여하는 것도 가능합니다.

복호화는 역방향의 단순한 치환입니다. 긴 토큰부터 순서대로 되돌림으로써 <氏名_1><氏名_11>의 접두사 충돌(Prefix collision)을 방지합니다.

def demask(text: str, mapping: dict[str, str]) -> str:
    for token in sorted(mapping, key=len, reverse=True):
        text = text.replace(token, mapping[token])
    ...

UI 측에서는 (a) 송신 전에 마스킹 내용과 탐지 목록을 제시하여 승인을 요청하고, (b) Claude의 응답은 복호화 전(플레이스홀더 상태)으로 표시하며, 사용자가 "복호화하기"를 눌렀을 때만 원래 값으로 되돌리는 2단계 확인 플로우로 구성했습니다. 이를 통해 "무엇이 외부로 나가는지", "언제 원래 정보로 되돌릴지"를 사용자가 완전히 제어할 수 있습니다.

8. 요약

Presidio는 탐지와 익명화가 느슨하게 결합(Loosely coupled)되어 있어 인식기(Recognizer)를 교체하기 쉬운 우수한 프레임워크이지만, 일본어에 적용하기 위해서는 다음과 같은 커스터마이징이 핵심이었습니다.

  • 단어 경계 \b를 lookaround로 교체
  • 일본의 정형 PII(개인식별정보)를 정규 표현식의 PatternRecognizer로 구현
  • 문맥어와 스코어(Score) 설계를 통해 "단순한 숫자열"의 모호함을 해소 (형태소 분석에도 주의)
  • 중복 해결 및 denylist를 통해 오탐지(False Positive) 및 과잉 탐지를 억제
  • 고유 플레이스홀더 + 매핑을 통해 가역 마스킹을 실현

LLM을 업무에 도입할 때의 공통 과제인 "개인정보를 어떻게 다룰 것인가"에 대해, Presidio는 현실적인 선택지가 될 수 있습니다. 실제 운영 시에는 대상 도메인의 샘플을 사용하여 임계값과 패턴을 튜닝하는 것을 권장합니다.

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0