LLM 호출 전 개인정보 마스킹 설계: 탐지·치환·복원 흐름과 Presidio 예시
마스킹 위치와 방식 고르기, 한국형 식별자 패턴 추가, 탐지 누락에 대비하는 운영
핵심 요약
- 개인정보는 LLM 호출 직전에 탐지·치환하고, 원본과 매핑 표는 서버 안에만 보관한다.
- 요약·분류에는 replace로 충분하고, 원래 값을 되돌려야 하면 매핑 표나 encrypt를 쓴다.
- 주민등록번호 같은 형식이 정해진 식별자는 PatternRecognizer로 추가하고 오탐·미탐을 측정한다.
- Presidio는 모든 민감 정보 탐지를 보장하지 않으므로 전송 필드 최소화와 공급자 조건 확인을 함께 한다.
상담 기록 요약, 민원 분류, 이메일 초안 작성처럼 LLM에 넘기는 텍스트에는 이름, 전화번호, 이메일, 주민등록번호가 섞여 있기 쉽다. 외부 LLM API로 이 데이터를 그대로 보내면 사내 개인정보 처리 기준과 계약 조건을 어길 수 있다. 해결책 중 하나는 호출 직전에 개인정보를 탐지해 가리고, 응답을 받은 뒤 필요한 경우에만 원래 값으로 되돌리는 것이다. 이 글은 오픈소스 도구 Presidio를 예로 이 흐름을 설계하는 방법을 정리한다. 사내 도구의 개인정보 점검 항목은 바이브 코딩 사내 도구 보안 글에서 다뤘다.
마스킹 흐름 전체 그림
- 탐지: 입력 텍스트에서 개인정보 위치와 종류를 찾는다.
- 치환: 찾은 값을 자리 표시자(
<PERSON_1>)나 가린 값으로 바꾼다. - LLM 호출: 치환된 텍스트만 외부로 보낸다.
- 복원(선택): 응답에 자리 표시자가 있으면 서버 안에서 원래 값으로 되돌린다.
- 기록: 원본·매핑 표는 서버 안에만 보관하고 로그에는 남기지 않는다.
치환 방식 고르기
| 방식(Presidio 연산자) | 결과 예 | 복원 | 적합한 경우 |
|---|---|---|---|
| replace | <PERSON> | 매핑 표를 따로 두면 가능 | 요약·분류처럼 실제 값이 필요 없을 때 |
| redact | 값 삭제 | 불가 | 값이 전혀 필요 없을 때 |
| mask | 010-****-5678 | 불가 | 일부만 보여 줘도 될 때 |
| hash | 해시 문자열 | 불가(같은 값은 같은 해시) | 동일 인물 여부만 구분할 때 |
| encrypt | 암호문 | 복호화로 가능 | 응답에서 원래 값을 되돌려야 할 때 |
Presidio 문서는 replace, redact, mask, hash, encrypt와 사용자 정의 연산자를 지원하며, 암호화한 데이터는 역익명화 기능으로 되돌릴 수 있다고 설명한다. 요약·분류 용도라면 대부분 replace로 충분하다.
1단계: 설치와 기본 사용
pip install presidio-analyzer presidio-anonymizer
# 분석기가 쓰는 NLP 모델은 문서 안내에 따라 별도로 설치·설정한다from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
text = "Contact John at john@example.com or 212-555-0100."
results = analyzer.analyze(text=text, language="en")
masked = anonymizer.anonymize(
text=text,
analyzer_results=results,
operators={
"PERSON": OperatorConfig("replace", {"new_value": "<PERSON>"}),
"EMAIL_ADDRESS": OperatorConfig("replace", {"new_value": "<EMAIL>"}),
"DEFAULT": OperatorConfig("redact"),
},
)
print(masked.text)연산자 설정 클래스의 import 경로는 버전에 따라 다를 수 있으므로 설치한 버전의 문서 예제를 확인한다. 분석기는 이름 같은 개체를 찾을 때 NLP 모델에 의존하므로, 한국어 텍스트의 이름 탐지는 한국어를 지원하는 NLP 엔진 설정이 별도로 필요하다. 문서는 설정 가능한 NLP 모델로 다국어를 지원한다고 안내한다.
2단계: 정규식 인식기로 한국형 식별자 추가
주민등록번호, 사업자등록번호, 국내 전화번호처럼 형식이 정해진 값은 패턴 인식기로 추가한다.
from presidio_analyzer import Pattern, PatternRecognizer
rrn = PatternRecognizer(
supported_entity="KR_RRN",
patterns=[Pattern(name="kr_rrn", regex=r"\b\d{6}-[1-4]\d{6}\b", score=0.85)],
)
kr_phone = PatternRecognizer(
supported_entity="KR_PHONE",
patterns=[Pattern(name="kr_mobile", regex=r"\b01[016789]-?\d{3,4}-?\d{4}\b", score=0.7)],
)
analyzer.registry.add_recognizer(rrn)
analyzer.registry.add_recognizer(kr_phone)정규식은 오탐(주문 번호를 전화번호로 인식)과 미탐(하이픈 없는 형식)을 함께 만든다. 실제 데이터 샘플로 두 비율을 측정하고, 오탐이 업무에 지장을 주지 않는 범위에서 패턴을 넓힌다.
3단계: 응답 복원은 서버 안에서만
LLM이 "<PERSON_1>님께 환불 안내를 보내세요" 같은 응답을 주면, 서버가 매핑 표로 원래 이름을 넣어 사용자 화면에만 보여 준다. 이를 위해 replace 대신 개체마다 번호를 붙인 자리 표시자를 만들고 매핑 표를 요청 단위로 메모리에 보관한 뒤 응답 처리 후 폐기한다. 매핑 표를 로그나 분석 도구에 남기면 마스킹한 의미가 사라진다.
탐지 누락에 대비하는 운영
Presidio 문서는 자동 탐지 방식이라 모든 민감 정보를 찾는다는 보장이 없다고 명시한다. 그래서 마스킹은 유일한 통제가 아니라 여러 층 중 하나로 둔다.
- 외부로 보내는 필드 자체를 줄인다. 요약에 필요 없는 고객 정보 필드는 처음부터 프롬프트에 넣지 않는다.
- LLM 공급자의 데이터 보존·학습 사용 조건을 계약과 설정에서 확인한다.
- 마스킹 전후 샘플을 주기적으로 검토해 새로 등장한 형식(새 전화번호 패턴, 외국인 등록번호)을 인식기에 추가한다.
- 요청·응답 로그를 남길 때는 마스킹된 텍스트만 저장한다.
운영 모니터링 항목은 LLMOps 모니터링 글을, 문서 검색 단계의 접근 통제는 사내 문서 RAG 접근 제어 글을 참고한다.
초보자가 자주 실수하는 포인트
- 마스킹 전 원문을 디버깅 로그에 남기는 경우
- 영어 NLP 모델만으로 한국어 이름이 탐지될 것이라고 가정하는 경우
- 정규식 패턴을 실제 데이터로 검증하지 않고 배포하는 경우
- 마스킹을 유일한 통제로 보고 프롬프트에 불필요한 고객 필드를 그대로 넣는 경우
체크리스트
- LLM에 보내는 필드가 작업에 필요한 것만 남았는가
- 국내 식별자 패턴 인식기를 추가했는가
- 오탐·미탐 비율을 샘플로 측정했는가
- 매핑 표가 요청 처리 후 폐기되는가
- 로그에 마스킹된 텍스트만 저장되는가
자주 묻는 질문
마스킹하면 LLM 답변 품질이 떨어지나요?
이름 같은 고유 정보가 필요 없는 요약·분류 작업에서는 영향이 작다. 자리 표시자에 번호를 붙이면 여러 인물을 구분할 수 있다.
이미지 속 개인정보도 처리할 수 있나요?
Presidio에는 OCR로 이미지 속 정보를 가리는 Image Redactor 구성 요소가 있다고 문서에 소개돼 있다.
해시와 암호화의 차이는 무엇인가요?
해시는 원래 값으로 되돌릴 수 없고 같은 값은 같은 결과가 된다. 암호화는 키로 복호화할 수 있어 원래 값 복원이 필요할 때 쓴다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.