AI 업무 활용

AI 도구에 붙여넣기 전에 이름·이메일·전화번호 마스킹하기

AI 채팅 도우미에 텍스트를 보내기 전에 눈에 띄는 개인정보를 제거하거나 대체하고, 마스킹된 결과와 탐지 보고서를 함께 검토해 놓친 항목을 찾습니다. 이 글은 작은 합성 예제를 사용하며 원본 파일은 변경하지 않습니다.

목차 보기

이런 분께이름, 이메일, 전화번호가 포함될 수 있는 업무 텍스트를 AI 도우미에 보내기 전에 실용적인 사전 점검을 하려는 사람을 위한 글입니다.

준비사항
  • Python 3.12
  • 텍스트 편집기와 AI 채팅 도우미

01작은 합성 메시지로 시작하기

이 예제는 전부 합성 데이터입니다. 이름, 이메일 주소, 전화번호, 회사 관련 정보, 메시지 내용은 모두 튜토리얼을 위해 만든 것입니다. 목적은 텍스트가 로컬 작업공간을 벗어나기 전에 눈에 띄는 개인정보를 제거하는 연습을 하는 것입니다.

다음 텍스트를 message.txt로 저장합니다. 이름 2개, 이메일 주소 2개, 전화번호 2개가 들어 있으며, 단순한 마스킹 규칙이 놓치도록 일부러 애매하게 만든 참조 1개도 포함되어 있습니다.

text
Project review notes

Alice Kim asked us to send the revised table to alice.kim@example.com.
If the file is delayed, call Alice at 010-1234-5678.

Bob Lee will review the second draft. His email is bob.lee@example.com and his office number is 02-345-6789.

The final approval should also be checked with A. Kim before Friday.

02민감한 원문이 아니라 마스킹 계획을 AI에 묻기

실제 민감한 문서를 보내지 않고도 AI 도우미에게 마스킹 규칙 설계 방법을 물어볼 수 있습니다. 탐지할 범주를 설명하고 실제 값 대신 가상의 예시를 사용합니다.

유용한 답변이라면 구조화된 식별자와 일반 자연어를 구분해야 합니다. 이메일 주소와 전화번호는 비교적 일정한 패턴을 가지는 경우가 많습니다. 반면 사람 이름은 더 어렵습니다. 일반 단어가 이름일 수도 있고, 이니셜이나 여러 문자 체계를 사용할 수도 있으며, 같은 사람이 여러 형태로 언급될 수 있습니다.

03AI 답변을 명시적인 마스킹 규칙으로 바꾸기

이 합성 예제에서는 이름에 대해 작은 승인 목록을 사용하고, 이메일 주소와 전화번호에는 정규식을 사용합니다. 이는 일반적인 개인정보 탐지기보다 의도적으로 범위를 좁힌 방식입니다.

데이터 유형규칙플레이스홀더
전체 이름승인된 정확한 이름 Alice Kim과 Bob Lee만 교체[NAME_1], [NAME_2]
이메일로컬 부분, @, 도메인을 포함하는 일반적인 주소 형태 탐지[EMAIL_1], [EMAIL_2]
전화번호이 예제에 등장하는 두 가지 전화번호 형식 탐지[PHONE_1], [PHONE_2]
기타 텍스트수동 검토를 위해 그대로 유지자동 교체 없음

일관된 플레이스홀더를 사용하면 원래 식별자를 남기지 않고도 반복되는 참조 관계를 이해할 수 있습니다. 아래 스크립트는 동일한 탐지 값이 반복되면 같은 플레이스홀더를 사용합니다.

04로컬 사본에 규칙 적용하기

다음 Python 스크립트는 message.txt를 읽고 승인한 규칙을 적용한 뒤 새 outputs 폴더에 결과를 저장합니다. 탐지된 원래 값과 플레이스홀더를 모두 담은 replacements.csv 보고서도 생성합니다. outputs가 이미 존재하면 중단하며 message.txt는 절대 덮어쓰지 않습니다.

python
import csv
import re
import sys
from pathlib import Path

INPUT = Path("message.txt")
OUTPUT_DIR = Path("outputs")
MASKED_FILE = OUTPUT_DIR / "message_masked.txt"
REPORT_FILE = OUTPUT_DIR / "replacements.csv"

KNOWN_NAMES = ["Alice Kim", "Bob Lee"]

EMAIL_RE = re.compile(
    r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"
)
PHONE_RE = re.compile(
    r"(?<!\d)(?:010-\d{4}-\d{4}|02-\d{3}-\d{4})(?!\d)"
)


def make_replacer(kind, mapping, rows):
    def replace(match):
        original = match.group(0)
        if original not in mapping:
            placeholder = f"[{kind}_{len(mapping) + 1}]"
            mapping[original] = placeholder
            rows.append(
                {
                    "type": kind,
                    "original": original,
                    "replacement": placeholder,
                }
            )
        return mapping[original]

    return replace


def mask_known_names(text, mapping, rows):
    for name in KNOWN_NAMES:
        if name not in text:
            continue
        if name not in mapping:
            placeholder = f"[NAME_{len(mapping) + 1}]"
            mapping[name] = placeholder
            rows.append(
                {
                    "type": "NAME",
                    "original": name,
                    "replacement": placeholder,
                }
            )
        text = text.replace(name, mapping[name])
    return text


def main():
    if not INPUT.is_file():
        sys.exit(f"Input file not found: {INPUT}")

    if OUTPUT_DIR.exists():
        sys.exit(f"Stop: {OUTPUT_DIR} already exists. Review or rename it first.")

    original = INPUT.read_text(encoding="utf-8")
    rows = []

    name_map = {}
    email_map = {}
    phone_map = {}

    masked = mask_known_names(original, name_map, rows)
    masked = EMAIL_RE.sub(make_replacer("EMAIL", email_map, rows), masked)
    masked = PHONE_RE.sub(make_replacer("PHONE", phone_map, rows), masked)

    OUTPUT_DIR.mkdir()
    MASKED_FILE.write_text(masked, encoding="utf-8")

    with REPORT_FILE.open("x", encoding="utf-8", newline="") as target:
        writer = csv.DictWriter(
            target,
            fieldnames=["type", "original", "replacement"],
        )
        writer.writeheader()
        writer.writerows(rows)

    print(f"Unique names masked: {len(name_map)}")
    print(f"Unique emails masked: {len(email_map)}")
    print(f"Unique phones masked: {len(phone_map)}")
    print(f"Masked copy: {MASKED_FILE}")
    print(f"Replacement report: {REPORT_FILE}")


if __name__ == "__main__":
    main()

mask_personal_data.py를 message.txt와 같은 폴더에 둔 뒤 python mask_personal_data.py로 실행합니다. 이 예제에서 예상되는 요약은 고유 이름 2개, 고유 이메일 2개, 고유 전화번호 2개가 마스킹되는 것입니다.

05원문과 예상 교체 결과 비교하기

결과를 신뢰하기 전에 예상 교체 내용을 먼저 직접 계산합니다. 이 스크립트는 고유하게 탐지된 값 6개를 기록해야 합니다.

유형원래 값교체 값
NAMEAlice Kim[NAME_1]
NAMEBob Lee[NAME_2]
EMAILalice.kim@example.com[EMAIL_1]
EMAILbob.lee@example.com[EMAIL_2]
PHONE010-1234-5678[PHONE_1]
PHONE02-345-6789[PHONE_2]

정확한 전체 이름 Alice Kim은 원문에 한 번 나타나며 [NAME_1]로 대체된다. 이후에 나오는 짧은 참조 Alice는 승인된 전체 이름 목록과 정확히 일치하지 않으므로 마스킹되지 않은 채 남으며 수동 검토에서 확인해야 한다. 보고서에는 각 발생 위치마다 한 행씩 기록하는 것이 아니라 감지된 고유 값이 포함된다.

06자동 규칙이 놓친 항목 검토하기

자동 교체는 첫 번째 단계일 뿐입니다. 어디에 붙여넣기 전에 outputs/message_masked.txt 전체를 직접 읽어야 합니다. 이 예제에서는 마지막 문단에 A. Kim이 그대로 남습니다. 승인 목록만으로는 스크립트가 이것이 Alice Kim을 가리킬 수 있다는 사실을 알 수 없습니다.

  1. 마스킹된 파일에서 @를 검색해 이메일 주소가 남아 있지 않은지 확인합니다.
  2. 010-과 02-를 검색해 예상한 두 전화번호 형식이 사라졌는지 확인합니다.
  3. Alice Kim과 Bob Lee를 검색해 승인된 전체 이름이 남아 있지 않은지 확인합니다.
  4. 각 줄을 직접 읽으며 이니셜, 별칭, 서명, 주소, 사번, 계정번호, 프로젝트 고유 식별자나 기타 식별 가능한 맥락이 남아 있는지 확인합니다.
  5. A. Kim이 남아 있다는 점을 확인합니다. 실제 문서에서 특정 인물을 식별할 수 있다면 수동으로 마스킹할지 결정합니다.
  6. replacements.csv를 원문과 비교해 각 교체 항목이 의도한 텍스트와 정확히 대응하는지 확인합니다.

07흔한 실수와 한계

  • 무엇을 마스킹해야 하는지 묻기 위해 원본 민감 텍스트를 그대로 AI 도구에 붙여넣지 않습니다.
  • 정규식이 임의의 사람 이름을 안정적으로 식별할 수 있다고 가정하지 않습니다.
  • 원문에 공백, 국가번호, 내선번호, 괄호 등 다른 형식이 있을 수 있다면 하나의 전화번호 패턴에만 의존하지 않습니다.
  • 마스킹된 텍스트 검토가 끝나기 전에 매핑 보고서를 삭제하지 않습니다. 다만 이 보고서에는 원래 식별자가 들어 있으므로 별도로 보호해야 합니다.
  • 원본 파일을 덮어쓰지 않습니다. 원본과 마스킹 사본을 분리해 보관합니다.
  • 세 가지 범주를 마스킹했다고 해서 기밀 비즈니스 정보, 자격증명, 금융정보, 건강정보나 기타 민감한 내용까지 제거되었다고 가정하지 않습니다.

실제 업무에서는 문서 성격, 적용 정책, AI 작업 목적에 따라 무엇을 제거해야 하는지 정의해야 합니다. 가능하다면 큰 문서를 전부 마스킹해 보내기보다 작업에 필요한 최소한의 텍스트만 제공하는 편이 더 안전합니다.

실행·검증 기록

ChatGPT Python tool · Python 3.x · standard library only

  • 첫 번째 실행은 성공적으로 완료되었다.
  • 콘솔은 고유 이름 2개, 고유 email 2개, 고유 phone number 2개가 마스킹되었다고 보고했다.
  • replacements.csv에는 정확히 6개의 행이 있었다: Alice Kim -> [NAME_1], Bob Lee -> [NAME_2], email 대체 2개, phone 대체 2개.
  • outputs/message_masked.txt에서는 승인된 두 full name, 두 email, 두 phone number가 모두 대체되었다.
  • 짧은 참조인 'Alice'는 코드가 승인된 정확한 이름 'Alice Kim'만 대체하므로 마스킹되지 않은 채 남았다.
  • 의도적으로 축약한 참조 'A. Kim'도 예상대로 마스킹되지 않은 채 남았다.
  • message.txt는 수정되지 않았다.
  • 두 번째 실행은 outputs가 이미 존재하여 중단되었고 덮어쓰기 방지 동작을 확인했다.
  • 2026-09-21 Python 3.12.14로 로컬 재검증: 수정된 예제가 통과했고 원본 보존과 덮어쓰기 방지도 확인했습니다.
검증 범위의 한계
  • full-name 로직은 의도적으로 두 이름만 포함한 정확 일치 allowlist이며 일반적인 개인정보 탐지기가 아니다.
  • Python 환경에서 관련 없는 스프레드시트 런타임 워밍업 경고가 발생했지만 스크립트 실행이나 생성 파일에는 영향을 주지 않았다.

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.