사내 RAG에 사번·주민번호 섞인 문서가 들어가는데 Claude API 호출 전에 어떻게 마스킹하나요?
사내 위키랑 인사 문서를 통째로 벡터 DB에 넣어서 RAG 챗봇을 만들고 있습니다. 그런데 문서 중에 사번, 주민번호 앞자리, 가끔 계좌번호까지 섞여 있는 걸 뒤늦게 발견했어요.
지금 구조는 retrieval로 청크 뽑아서 Claude API로 그대로 프롬프트에 넣는 방식인데, 이러면 외부 API로 개인정보가 그대로 나가는 거잖아요. 법무팀에서도 걸렸습니다.
질문이 두 갈래입니다.
- 임베딩/인덱싱 단계에서 마스킹할지, LLM 호출 직전에 마스킹할지
- 정규식만으로 충분한지, 아니면 별도 PII 탐지 모델을 붙여야 하는지
실제 운영하시는 분들 어떻게 처리하세요?
답변 3개
- 채택된 답변에디터 검증
양자택일 아니고 두 단계 다 거셔야 합니다. 한 군데만 걸면 반드시 구멍 나요. 저도 인덱싱에서만 처리했다가 query에 PII 들어오는 케이스 놓친 적 있어서요.
인덱싱 시점엔 원본 말고 마스킹된 버전을 임베딩하세요. 단 주의할 게, 너무 공격적으로 날리면 검색 품질이 떨어집니다. 주민번호/계좌 같은 식별자는
[RRN]토큰으로 통째 치환해도 검색 영향 거의 없는데, 사람 이름까지 다 날리면 "김부장이 승인한 건" 같은 질의가 안 먹어요. 그래서 보통 식별자(주민번호/계좌/카드/전화)는 인덱싱 전에 치환, 이름 같은 준식별자는 원본 두고 응답 단에서 정책 적용으로 갑니다.LLM 호출 직전에 retrieval된 청크를 한 번 더 통과시킵니다. 인덱싱 때 놓친 거 + 사용자 질의 자체에 들어온 PII 둘 다 여기서 막아요.
정규식만으론 부족합니다. 한국 주민번호는
\d{6}[-\s]?\d{7}로 1차는 꽤 잡히는데, 체크섬 검증 안 하면 전화번호나 날짜 조합을 오탐해요. 주민번호는 마지막 자리 체크섬(가중치 2,3,4,5,6,7,8,9,2,3,4,5 합 mod 11) 검증 꼭 넣으세요. 계좌번호는 은행별로 자릿수가 달라서 순수 정규식으론 지옥입니다.저희는 1차 정규식(빠르고 결정적) + 2차로 한국어 NER 모델 돌리는 2단으로 갑니다. 오픈소스 한국어 개체명 인식 파인튜닝 모델 쓰거나, 상용 PII 탐지 API 붙이거나. 정규식 단독은 식별자 패턴(주민/계좌/카드) 잡는 데까진 좋은데, 문맥 의존 PII(이름·주소)는 모델 없으면 새요.
마스킹 얘기 다 나왔는데, 구조 자체를 다시 보시는 것도 권해요. 인사 문서랑 일반 위키를 같은 벡터 DB, 같은 컬렉션에 넣은 게 사실 더 큰 문제입니다.
인사/민감 문서는 별도 컬렉션으로 분리하고, 사용자 권한에 따라 retrieval 대상 컬렉션을 제한하세요. 마스킹은 최후의 방어선이고, 애초에 권한 없는 사람한테 그 청크가 아예 안 뽑히게 하는 게 1차 방어선이에요. 마스킹 한 겹만 믿다가 그 한 겹이 실패하면 끝장이라.
마스킹 로직은 위 답변이 정확하고, 거기에 제일 자주 새는 구멍 하나 더 — 로그입니다.
마스킹 아무리 잘해도 디버깅용 프롬프트 로그에 원본 그대로 찍으면 거기서 다 샙니다. Sentry나 Datadog에 프롬프트 통째로 보내는 코드 다들 짜잖아요. 거기 페이로드에 주민번호 박혀서 외부 APM으로 나가면 그게 또 유출이에요. 로깅/APM으로 나가는 페이로드도 마스킹 파이프라인 뒤에 둬야 합니다. 이거 진짜 놓치기 쉬워요.
그리고 법무 걸린 거면 위탁 이슈도 같이 보세요. 외부 LLM API로 개인정보 보내는 건 개인정보 처리위탁이라 처리방침에 수탁자(Anthropic 등) 명시하고 동의 받았는지 확인해야 합니다. 근데 마스킹해서 식별 불가능하게 만들면 애초에 '개인정보'가 아니게 돼서 이 이슈를 상당 부분 회피할 수 있어요. 그래서 법무 관점에서도 식별자 치환이 정답입니다.