익명화 전략

발행: (2026년 6월 10일 AM 10:10 GMT+9)
8 분 소요
원문: Dev.to

출처: Dev.to

감지는 PII가 어디에 있는지 알려줍니다. 익명화는 그에 대해 무엇을 할지 결정합니다. Presidio의 익명화 엔진은 다섯 가지 내장 연산자를 제공하며, 각각은 서로 다른 규정 요구사항 및 사용 사례에 맞춰 설계되었습니다. 잘못된 연산자를 선택하면 복구가 필요한 데이터를 파괴하거나 의도하지 않은 방식으로 민감한 정보가 노출될 수 있습니다.

이 파트에서는 모든 익명화 연산자를 소개하고, 각각을 언제 사용해야 하는지, 일관된 이름 매핑을 통한 가명화(pseudonymization) 구축 방법, 그리고 PDF에서 PII를 처리하는 방법을 다룹니다.

다섯 가지 내장 연산자

Replace

감지된 엔터티를 지정된 값으로 교체합니다. 기본 연산자입니다.

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig

analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()

text = "John Smith called from 206-555-0147 about his account."

results = analyzer.analyze(text=text, language="en")

# 엔터티 타입 라벨로 교체 (기본 동작)
anonymized = anonymizer.anonymize(
    text=text,
    analyzer_results=results,
    operators={
        "PERSON": OperatorConfig("replace", {"new_value": "[REDACTED NAME]"}),
        "PHONE_NUMBER": OperatorConfig("replace", {"new_value": "[REDACTED PHONE]"})
    }
)

print(anonymized.text)
# Output: [REDACTED NAME] called from [REDACTED PHONE] about his account.

인간이 읽을 수 있는 형태를 유지하고 원본 값을 복구할 필요가 없을 때 replace를 사용합니다. 외부 팀과 익명화된 데이터셋을 공유하거나, 대시보드에 정제된 텍스트를 표시하고, PII 유형은 중요하지만 실제 값은 필요 없는 감사 로그에 적합합니다.

Redact

엔터티를 완전히 제거하고 자리표시자도 남기지 않습니다.

anonymized = anonymizer.anonymize(
    text=text,
    analyzer_results=results,
    operators={
        "PERSON": OperatorConfig("redact"),
        "PHONE_NUMBER": OperatorConfig("redact")
    }
)

print(anonymized.text)
# Output:  called from  about his account.

redact는 텍스트 구조를 바꾸어 문장이 읽기 어려워질 수 있습니다. 가독성이 우선이 아닌 내부 감사 로그, PII 흔적을 전혀 남기면 안 되는 엄격한 규정 상황, 인간에게 텍스트가 보여지지 않는 자동 파이프라인 등에 적합합니다.

Mask

각 문자를 마스킹 문자로 교체해 원본 값의 길이를 유지합니다.

anonymized = anonymizer.anonymize(
    text=text,
    analyzer_results=results,
    operators={
        "PERSON": OperatorConfig("mask", {
            "masking_char": "*",
            "chars_to_mask": 100,  # 모든 문자 마스킹
            "from_end": False
        }),
        "PHONE_NUMBER": OperatorConfig("mask", {
            "masking_char": "#",
            "chars_to_mask": 8,    # 앞 8자리 마스킹
            "from_end": False
        })
    }
)

print(anonymized.text)
# Output: ********** called from ########47 about his account.

길이 또는 일부 값만 보존해야 할 때 유용합니다. 예를 들어 신용카드 영수증에 마지막 네 자리만 표시하거나, 상담원이 부분 식별자를 확인해야 하는 지원 화면 등에 활용됩니다.

Hash

엔터티를 단방향 해시값으로 교체합니다. 동일 입력은 항상 동일 해시를 생성하므로 원본 PII를 노출하지 않으면서도 분석에 활용할 수 있습니다.

anonymized = anonymizer.anonymize(
    text=text,
    analyzer_results=results,
    operators={
        "PERSON": OperatorConfig("hash", {"hash_type": "sha256"}),
        "PHONE_NUMBER": OperatorConfig("hash", {"hash_type": "sha256"})
    }
)

print(anonymized.text)
# Output: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f called from ...

hashsha256(기본)과 sha512를 지원합니다. 해시는 복구가 불가능하지만, 두 레코드가 동일 인물인지 여부를 해시 비교만으로 판단할 수 있습니다. 분석 파이프라인, 중복 제거, 익명화된 데이터셋 간 교차 참조 등에 적합합니다.

Encrypt

엔터티를 암호화된 값으로 교체합니다. 올바른 키가 있으면 나중에 복호화가 가능합니다.

anonymized = anonymizer.anonymize(
    text=text,
    analyzer_results=results,
    operators={
        "DEFAULT": OperatorConfig("encrypt", {"key": "WmZq4t7w!z%C*F-J"})
    }
)

print(anonymized.text)
# Entities replaced with base64-encoded encrypted strings

encrypt는 유일하게 복원 가능한 연산자입니다. 이후에 다음과 같이 복원할 수 있습니다:

from presidio_anonymizer import DeanonymizeEngine
from presidio_anonymizer.entities import OperatorConfig

deanonymizer = DeanonymizeEngine()

deanonymized = deanonymizer.deanonymize(
    text=anonymized.text,
    entities=anonymized.items,
    operators={
        "DEFAULT": OperatorConfig("decrypt", {"key": "WmZq4t7w!z%C*F-J"})
    }
)

print(deanonymized.text)
# Output: John Smith called from 206-555-0147 about his account.

LLM에 데이터를 전송하기 전에 정제하고, 이후 복호화하는 PII 프록시 패턴에 encrypt/decrypt를 사용합니다. 파트 5에서 정확히 이 파이프라인을 구현할 예정입니다.

엔터티 유형별 연산자 혼합

실제 상황에서는 같은 문서 내에서도 엔터티 유형마다 다른 전략을 적용하고 싶을 때가 많습니다.

operators = {
    "PERSON": OperatorConfig("replace", {"new_value": ""}),
    "EMAIL_ADDRESS": OperatorConfig("hash", {"hash_type": "sha256"}),
    "PHONE_NUMBER": OperatorConfig("mask", {
        "masking_char": "*",
        "chars_to_mask": 8,
        "from_end": False
    }),
    "CREDIT_CARD": OperatorConfig("encrypt", {"key": "WmZq4t7w!z%C*F-J"}),
    "US_SSN": OperatorConfig("redact"),
    "DEFAULT": OperatorConfig("replace", {"new_value": ""})
}

DEFAULT 연산자는 특정 연산자가 지정되지 않은 모든 엔터티 유형을 잡아냅니다. 처리되지 않은 엔터티가 남지 않도록 항상 기본값을 설정하세요.

일관된 매핑을 통한 가명화(Pseudonymization)

일반 replace는 매번 다른 자리표시자를 생성합니다. 예를 들어 문서에 “John Smith”가 세 번 등장하면 각각 다른 <PERSON> 라벨이 붙습니다. 이는 레드액션에는 괜찮지만, 레코드 간에 동일 인물을 추적해야 하는 분석에는 부적합합니다.

가명화는 각 고유 값을 일관된 가짜 값으로 매핑합니다. “John Smith”는 항상 “Robert Chen”이 되고, “Jane Doe”는 항상 “Maria Santos”가 됩니다. 매핑은 데이터셋 내에서 일관성을 유지하지만, 매핑 테이블 없이는 원본 값을 복구할 수 없습니다.

from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig
from faker import Faker

fake = Faker()
Faker.seed(42)  # 재현 가능한 가짜 데이터

# 일관성을 위한 매핑 저장소
pii_mapping = {}

def get_consistent_replacement(original, entity_type):
    key = f"{entity_type}:{original}"
    if key not in pii_mapping:
        if entity_type == "PERSON":
            pii_mapping[key] = fake.name()
        elif entity_type == "EMAIL_ADDRESS":
            pii_mapping[key] = fake.email()
        elif entity_type == "PHONE_NUMBER":
            pii_mapping[key] = fake.phone_number()
        elif entity_type == "LOCATION":
            pii_mapping[key] = fake.city()
        else:
            pii_mapping[key] = f"[{entity_type}_{len(pii_mapping)}]"
    return pii_mapping[key]

Presidio와 통합하려면 커스텀 연산자를 만들거나 결과를 후처리하면 됩니다:

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEng
0 조회
Back to Blog

관련 글

더 보기 »