LLM 엔지니어링21장 · AI 거버넌스와 보안, 온프레미스 인프라8 / 22← 이전목차다음 →MetaContents

21장. AI 거버넌스와 보안, 온프레미스 인프라

개인정보 찾기와 가리기

병원 접수 창구는 진료 기록을 연구팀에 넘길 때 이름과 주민등록번호를 지우고 "환자 A"처럼 바꿉니다. 같은 환자의 기록은 늘 "환자 A"로 적어야 연구는 할 수 있습니다. 상담 기록을 LLM 에 보내거나 로그에 남길 때도 같은 일을 합니다.

개인정보 찾기, 마스킹, 가명 처리, 권한 있는 사람만 되돌리기의 네 단계 흐름도

1번에서 글 속의 전화번호, 이메일, 주민등록번호, 카드, 계좌를 찾습니다. 2번 마스킹은 [전화] 처럼 종류만 남기고 가립니다. 3번 가명 처리는 같은 값을 늘 같은 가짜 표시([전화_3f2a])로 바꿉니다. 4번에서 권한 있는 사람만 원래 값을 되찾습니다. "환자 A"가 가명 처리입니다.

개인정보의 꼴

종류꼴함정
전화010-0000-1234, 01000001234, 010 0000 1234하이픈 없는 것, 띄어 쓴 것
이메일user@example.com비교적 쉽다
주민등록번호900101-1234567주문번호 20261019-1835668 의 뒷부분과 닮았다
카드4111-1111-1111-1111, 4012 8888 8888 1881띄어 쓴 것
계좌501-73-525265은행마다 자리 수가 다르고 날짜(2026-10-22)와 닮았다
이름김하늘정규식으로는 거의 잡을 수 없다. 개체명 인식 모델이 필요하다

정규식으로 찾기

Python
import re

memo = "주문번호 20261019-1835668, 연락처 010 0000 4487, 생년 900101-1234567"
naive = re.compile(r"\d{6}-\d{7}")                     # 주민번호처럼 생긴 것
careful = re.compile(r"(?<!\d)\d{6}-[1-4]\d{6}(?!\d)")  # 앞뒤에 숫자가 붙지 않고, 뒷자리 첫 숫자가 1~4
# 결과: 경계를 두지 않으면 주문번호의 뒷부분까지 주민번호로 잡는다
print("단순:", naive.findall(memo))
print("경계:", careful.findall(memo))

코드에 나오는 함수

코드쉬운 뜻
\ddigit(숫자)의 줄임말이다. 0~9 가운데 한 글자다
{6}, {7}바로 앞 꼴이 정확히 6번, 7번 이어진다
[1-4]1, 2, 3, 4 가운데 한 글자다
(?<!\d)negative lookbehind(뒤돌아보아 아니기)다. 바로 앞 글자가 숫자가 아닐 때만 맞는다. 그 글자는 결과에 넣지 않는다
(?!\d)negative lookahead(앞을 보아 아니기)다. 바로 뒤 글자가 숫자가 아닐 때만 맞는다
re.findall(...)맞는 자리를 모두 찾아 목록으로 돌려준다

이 코드가 뜻하는 것

주차장의 번호판 인식 카메라를 떠올려 봅니다. "숫자 여섯-숫자 일곱"처럼 생긴 것만 찾으면, 옆 트럭에 적힌 긴 전화번호의 끝부분까지 번호판으로 읽습니다. 그래서 번호판 테두리, 곧 앞뒤에 다른 숫자가 붙어 있지 않은지도 함께 봅니다. 여기서 번호판 꼴이 \d{6}-\d{7}, 테두리 확인이 (?<!\d) 와 (?!\d) 입니다.

줄하는 일쉽게 말하면
memo = ...주문번호, 연락처, 생년(모두 가짜)이 섞인 메모여러 숫자가 적힌 주차장
naive = ...꼴만 보는 정규식테두리를 안 보는 카메라
careful = ...앞뒤 경계와 뒷자리 첫 숫자까지 보는 정규식테두리를 보는 카메라

단순 정규식은 주문번호 20261019-1835668 에서 앞의 '20' 을 버리고 261019-1835668 을 주민번호로 잡았습니다. 경계를 둔 정규식은 900101-1234567 하나만 잡았습니다.

작게 줄여서 눈으로 보기

경계와 [1-4] 가 하나씩 무엇을 걸러 내는지, 지어낸 숫자 넷으로 봅니다.

Python
import re

for s in ["900101-1234567", "20261019-1835668", "900101-9234567", "900101-12345678"]:
    plain = re.findall(r"\d{6}-\d{7}", s)
    edge = re.findall(r"(?<!\d)\d{6}-[1-4]\d{6}(?!\d)", s)
    print(f"{s:17s} 단순={plain} 경계={edge}")
출력
900101-1234567    단순=['900101-1234567'] 경계=['900101-1234567']
20261019-1835668  단순=['261019-1835668'] 경계=[]
900101-9234567    단순=['900101-9234567'] 경계=[]
900101-12345678   단순=['900101-1234567'] 경계=[]
결과읽는 법
900101-1234567꼴이 맞아 둘 다 잡는다
20261019-1835668단순은 가운데부터 잡는다. 경계는 바로 앞에 숫자 '0' 이 붙어 있어 버린다
900101-9234567뒷자리 첫 숫자가 9 라 [1-4] 에 맞지 않는다
900101-12345678뒷자리가 8자리다. 단순은 앞 7자리만 떼어 잡고, 경계는 뒤에 숫자 '8' 이 붙어 있어 버린다

경계를 두면 "더 긴 숫자의 일부"를 잘못 잡는 일이 줄어듭니다. 그래도 정규식은 꼴만 보므로, 꼴이 같은 다른 번호는 여전히 잡을 수 있습니다.

주의할 점

  • 이 장의 개인정보는 모두 지어낸 것입니다. 전화번호는 가운데를 0000 으로, 이메일은 example.com 으로, 카드는 결제 시험용으로 공개된 번호로 두었습니다. 실제 개인정보로 실습하지 않습니다.
  • 개인정보는 받지 않는 것이 가장 안전합니다. 상담 화면에서 주민등록번호 칸을 없애는 것이 가리는 코드보다 낫습니다. 법(개인정보 보호법)이 정한 처리 기준도 확인합니다.
← 이전 절21장 · LLMSecOps다음 절 →21장 · 접근 제어