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]을 사용해야 합니다. 보고서에는 각 등장 횟수가 아니라 고유하게 탐지된 값만 기록됩니다.

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 작업 목적에 따라 무엇을 제거해야 하는지 정의해야 합니다. 가능하다면 큰 문서를 전부 마스킹해 보내기보다 작업에 필요한 최소한의 텍스트만 제공하는 편이 더 안전합니다.

실행·검증 기록

2026-09-21 · hand-checked example · Python 3.12

  • 합성 원문과 예상 교체 값을 직접 확인했습니다.
  • 예상 탐지 집합은 고유 전체 이름 2개, 고유 이메일 주소 2개, 고유 전화번호 2개입니다.
  • Alice Kim은 두 번 등장하지만 일관되게 [NAME_1]으로 매핑되어야 합니다.
  • 의도적으로 축약한 참조 A. Kim은 마스킹되지 않고 남아 있어 미탐 사례를 보여줍니다.
  • 스크립트는 message.txt를 읽고 outputs/message_masked.txt와 outputs/replacements.csv에만 씁니다.
  • outputs가 이미 존재하면 이전 검토 결과를 덮어쓰지 않고 중단합니다.
검증 범위의 한계
  • Python 코드는 직접 실행하지 않았으며, 작은 예제와 예상 결과를 손으로 검토했습니다.
  • 전체 이름 탐지는 명시적인 두 이름 목록을 사용하며 일반적인 개체명 인식 시스템이 아닙니다.
  • 이메일 정규식은 일반적인 주소 형태를 대상으로 하며 모든 유효한 이메일 문법을 구현하기 위한 것이 아닙니다.
  • 전화번호 정규식은 이 합성 예제에서 사용하는 두 형식만 다룹니다.
  • 눈에 띄는 식별자를 마스킹했다고 해서 특정 개인정보보호법이나 조직 정책의 준수 또는 완전한 익명화가 보장되는 것은 아닙니다.

사이트 전체의 작성·검증 원칙

참고 출처

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