PII 마스킹 누락, 배포 전에 자동으로 잡아내기
Presidio와 CI 파이프라인으로 구축하는 마스킹 누락 탐지 시스템
솔직히 고백하면, 저도 한때 마스킹 규칙을 스프레드시트로 관리했습니다. DB에 컬럼 하나 추가할 때마다 "마스킹 규칙 업데이트했나?" 하고 슬랙에 물어보는 게 프로세스의 전부였죠. 그러다 어느 날 스테이징 환경 로그에서 고객 전화번호가 평문으로 찍히고 있는 걸 발견했습니다. 새 컬럼을 추가한 지 2주가 지나서야요. 감사팀에서 먼저 발견하지 않은 게 다행이었습니다.
이 경험 이후로 마스킹 규칙의 완전성을 자동으로 검증하는 파이프라인을 구축하게 됐고, 지금은 PR 단계에서 마스킹 누락이 잡히지 않으면 아예 머지가 안 되는 구조로 운영하고 있습니다. 이 글에서는 PII 탐지 도구와 CI를 연동해서, 마스킹 규칙에 빈틈이 생기는 순간 배포를 차단하는 파이프라인을 처음부터 끝까지 구축하는 과정을 공유합니다. CI/CD 파이프라인을 직접 다루는 백엔드·인프라 개발자라면 바로 적용 가능한 내용이고, 그 외 역할이라면 "우리 팀에 이런 자동화가 필요하겠다"는 감을 잡는 용도로 읽어보시면 좋겠습니다.
핵심 개념
"Shift-Left Privacy" — 마스킹 검증을 개발 단계로 끌어오기
기존 방식은 프로덕션 배포 후 정기 감사에서 마스킹 누락을 발견하는 구조였습니다. 문제는 이미 데이터가 노출된 뒤라는 거죠. Shift-Left Privacy는 이 검증을 개발 단계, 구체적으로는 pre-commit hook이나 CI 파이프라인으로 끌어와서 코드가 머지되기 전에 누락을 잡아내는 전략입니다.
저도 처음엔 "SAST 돌리듯이 PII 스캔도 그냥 하나 더 얹으면 되는 거 아닌가?" 싶었는데, 실제로 그 직관이 맞습니다. PII 탐지를 SAST와 동급으로 CI에 내장하는 흐름이 점점 더 흔해지고 있고, Checkmarx와 HoundDog.ai의 파트너십이 좋은 사례입니다. 이 둘은 기존 AppSec 커버리지에 PII 유출 탐지를 기본 항목으로 포함시켜서, 코드가 머지되기 전에 "이 변수에 담긴 이메일이 로그로 빠지고 있다"는 걸 정적 분석으로 잡아냅니다.
GDPR은 이미 익숙한 이야기이고, 2025년부터 본격 시행된 EU AI Act까지 겹치면서 AI 학습 데이터의 PII 관리가 법적 의무가 됐습니다. Verizon의 2024 DBIR(Data Breach Investigations Report)에 따르면 전체 데이터 유출 사고의 약 50% 이상이 개인정보를 포함하고 있었습니다. 시크릿 관리조차 85%의 조직이 소스 코드에 평문으로 박아두고 있는 상태니, PII 관리는 더 열악할 수밖에 없겠죠. 자동화 없이 사람 머리로만 관리하는 건 이미 한계를 넘었습니다.
마스킹 완전성 검증의 3단계 구조
파이프라인의 뼈대를 처음 설계할 때 "대체 어디부터 시작하지?" 싶었는데, 막상 정리해보니 세 단계로 깔끔하게 나뉘었습니다.
[1. PII 탐지(Discovery)] → [2. 마스킹 규칙 매핑(Mapping)] → [3. 누락 검출(Gap Detection)]| 단계 | 하는 일 | 핵심 기술 |
|---|---|---|
| PII 탐지 | 코드·스키마·로그에서 민감 데이터 필드를 식별 | NER, 정규표현식, 체크섬 검증 |
| 규칙 매핑 | 탐지된 PII 필드에 마스킹 규칙이 적용되어 있는지 대조 | 마스킹 규칙 YAML + 스키마 diff |
| 누락 검출 | 규칙이 없는 필드를 "미커버" 상태로 분류하고 빌드를 실패시킴 | CI 게이트, 알림 연동 |
핵심 원칙: 마스킹 규칙을 코드처럼 버전 관리하고, 스키마가 변경될 때마다 자동으로 커버리지를 재검증하는 것이 이 파이프라인의 존재 이유입니다.
마스킹 전략 4가지 — 언제 어떤 걸 쓸까?
마스킹 규칙을 작성하다 보면 MASK, REDACT, HASH, ENCRYPT 중 뭘 써야 할지 고민되는 순간이 옵니다. 각각 용도가 다릅니다.
| 전략 | 동작 | 사용 예시 | 원본 복원 |
|---|---|---|---|
| MASK | 일부 문자를 *로 치환 |
us***@example.com |
불가 |
| REDACT | 값 전체를 [REDACTED]로 대체 |
로그 출력, 디버깅 환경 | 불가 |
| HASH | SHA-256 등으로 단방향 변환 | 분석용 ID 일관성 유지가 필요할 때 | 불가 (단방향) |
| ENCRYPT | AES-256 등으로 양방향 암호화 | 권한 있는 사용자만 복호화해야 할 때 | 키 보유 시 가능 |
실무에서 자주 쓰는 조합은 이렇습니다. 이메일·전화번호처럼 "형태는 보여줘야 하는데 전체는 안 되는" 경우엔 MASK, 로그에 찍히면 안 되는 건 REDACT, 분석 파이프라인에서 동일 사용자를 추적해야 하면 HASH, 내부 관리자가 원본을 봐야 하는 경우엔 ENCRYPT를 적용합니다.
NER + 정규식 + 도메인 규칙: 왜 다층 탐지가 필요한가
실무에서 자주 맞닥뜨리는 상황인데, 정규표현식만으로는 "김철수"라는 이름이 PII인지 일반 텍스트인지 구분할 수 없습니다. 반대로 NER 모델만 쓰면 "010-1234-5678" 같은 명확한 패턴도 문맥에 따라 놓칠 수 있고요. 그래서 Microsoft Presidio 같은 도구가 NER, 정규식, 체크섬 검증을 조합한 하이브리드 접근을 취하는 겁니다.
탐지 레이어 구성:
Layer 1: 정규표현식 → 전화번호, 이메일, 주민번호 등 정형 패턴
Layer 2: NER 모델 → 사람 이름, 주소 등 비정형 텍스트
Layer 3: 도메인 규칙 → 한국 주민번호 체크섬, 사업자등록번호 검증식
Layer 4: 컨텍스트 → 주변 단어("연락처:", "성함:") 기반 신뢰도 보정용어 정리 — NER(Named Entity Recognition): 텍스트에서 사람 이름, 장소, 조직명 등 고유명사를 자동으로 식별하는 자연어 처리 기술입니다. PII 탐지에서는 이를 확장하여 전화번호, 주소 등 개인 식별이 가능한 모든 정보를 인식하는 데 활용됩니다.
실전 적용
예시 1: Pre-commit Hook으로 코드 내 PII 하드코딩 원천 차단
가장 적은 노력으로 가장 빠른 효과를 볼 수 있는 출발점입니다. 테스트 코드에 user@test.com이나 010-1234-5678 같은 실제 PII를 박아두는 경우, 의외로 많거든요.
저도 "어차피 테스트 코드인데 뭐" 하면서 실제 이메일을 넣은 적이 있습니다. 문제는 그 테스트가 CI에서 돌면서 로그에 이메일이 그대로 찍혔고, 그 로그를 모니터링 시스템이 수집해서 팀 전체가 볼 수 있는 대시보드에 올라갔다는 거예요. 테스트 코드라고 방심했다가 꽤 민망한 상황이 됐습니다. pre-commit hook을 걸어두면 커밋 자체가 막히니 아예 습관이 바뀝니다.
# .pre-commit-config.yaml
repos:
# PII 탐지 — 코드 내 개인정보 하드코딩 차단
- repo: https://github.com/uktrade/pii-secret-check-hooks
rev: v0.5.0
hooks:
- id: pii_secret_filename_check
- id: pii_secret_content_check
# 시크릿 탐지 — API 키, 비밀번호 등 유출 차단
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
hooks:
- id: gitleaks실무 팁: PII 탐지와 시크릿 탐지(
gitleaks)는 목적이 다르지만 파이프라인에서 병렬 실행하면 효과적입니다. PII는 개인정보 보호, 시크릿은 접근 권한 보호라는 차이가 있지만, 둘 다 "코드에 있으면 안 되는 민감 정보"라는 공통점이 있습니다.
인프라 담당자가 한 번 .pre-commit-config.yaml을 세팅해두면, 팀원들은 pip install pre-commit && pre-commit install만 실행하면 됩니다. 이후로는 커밋할 때마다 자동으로 스캔이 돌아갑니다.
예시 2: GitHub Actions + Presidio로 PR 단계에서 마스킹 누락 차단하기
이 파이프라인의 핵심입니다. DB 스키마 파일과 마스킹 규칙 YAML을 대조해서, 새로 추가된 컬럼이 마스킹 규칙에 포함되지 않으면 PR을 차단하는 워크플로입니다. 처음 이걸 세팅할 때 "스키마 파싱이 복잡하겠다" 싶었는데, SQL DDL에서 테이블·컬럼만 뽑아내면 되니까 생각보다 단순했습니다.
# .github/workflows/pii-check.yml
name: PII Masking Verification
on: [pull_request]
jobs:
pii-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install presidio-analyzer presidio-anonymizer pyyaml
- name: Verify Masking Completeness
run: |
python scripts/scan_schema_coverage.py \
--schema-dir ./db/schemas \
--masking-rules ./config/masking-rules.yaml \
--fail-on-uncovered마스킹 규칙은 이런 YAML 형태로 스키마 파일 옆에 두고 함께 버전 관리합니다.
# config/masking-rules.yaml
tables:
users:
email: MASK
phone: REDACT
name: HASH
address: ENCRYPT
orders:
shipping_address: MASK
recipient_name: HASH그리고 핵심인 스키마-규칙 대조 스크립트입니다. 초안을 작성할 때 이 부분을 블랙박스로 남겨뒀다가, 결국 "이게 없으면 글의 의미가 없지 않나" 싶어서 직접 공유합니다.
# scripts/scan_schema_coverage.py
import argparse
import re
import sys
from pathlib import Path
import yaml
def extract_columns_from_ddl(schema_dir: str) -> dict[str, list[str]]:
"""SQL DDL 파일에서 테이블별 컬럼 목록을 추출"""
tables = {}
create_re = re.compile(
r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s*\(", re.IGNORECASE
)
column_re = re.compile(r"^\s+(\w+)\s+\w+", re.MULTILINE)
for sql_file in Path(schema_dir).glob("**/*.sql"):
content = sql_file.read_text()
for match in create_re.finditer(content):
table_name = match.group(1)
start = match.end()
depth, end = 1, start
for i in range(start, len(content)):
if content[i] == "(":
depth += 1
elif content[i] == ")":
depth -= 1
if depth == 0:
end = i
break
body = content[start:end]
skip = {"constraint", "primary", "foreign", "unique", "index", "check"}
columns = [
m.group(1)
for m in column_re.finditer(body)
if m.group(1).lower() not in skip
]
tables[table_name] = columns
return tables
def load_masking_rules(rules_path: str) -> dict[str, list[str]]:
"""YAML에서 마스킹 규칙이 정의된 컬럼 목록을 로드"""
with open(rules_path) as f:
config = yaml.safe_load(f)
return {
table: list(columns.keys())
for table, columns in config.get("tables", {}).items()
}
def find_uncovered(schema_dir: str, rules_path: str) -> list[dict]:
"""마스킹 규칙이 없는 컬럼을 찾아 반환"""
schema_tables = extract_columns_from_ddl(schema_dir)
rule_tables = load_masking_rules(rules_path)
uncovered = []
for table, columns in schema_tables.items():
covered = set(rule_tables.get(table, []))
for col in columns:
if col not in covered and col not in ("id", "created_at", "updated_at"):
uncovered.append({"table": table, "column": col})
return uncovered
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--schema-dir", required=True)
parser.add_argument("--masking-rules", required=True)
parser.add_argument("--fail-on-uncovered", action="store_true")
parser.add_argument("--warn-only", action="store_true")
args = parser.parse_args()
uncovered = find_uncovered(args.schema_dir, args.masking_rules)
if not uncovered:
print("All columns are covered by masking rules.")
return
print(f"\n{'!'*50}")
print(f" {len(uncovered)} uncovered column(s) detected:")
print(f"{'!'*50}\n")
for item in uncovered:
print(f" - {item['table']}.{item['column']}")
if args.fail_on_uncovered and not args.warn_only:
sys.exit(1)
else:
print("\n[WARN] Running in warn-only mode. CI will not fail.")
if __name__ == "__main__":
main()| 구성 요소 | 역할 |
|---|---|
extract_columns_from_ddl |
SQL DDL 파일을 파싱해서 테이블별 컬럼 목록을 추출합니다 |
load_masking_rules |
YAML에 정의된 마스킹 규칙의 테이블·컬럼 목록을 로드합니다 |
find_uncovered |
스키마에는 있지만 마스킹 규칙에는 없는 컬럼을 찾아냅니다 |
--fail-on-uncovered |
미커버 컬럼이 있으면 exit code 1로 종료하여 PR을 차단합니다 |
--warn-only |
경고만 출력하고 CI를 통과시킵니다 (도입 초기에 유용) |
이 구성의 핵심은 스키마 변경이 있는 PR에서 자동으로 마스킹 규칙 커버리지를 검증한다는 점입니다. 개발자가 ALTER TABLE users ADD COLUMN birthday DATE;를 추가했는데 마스킹 규칙에 birthday를 안 넣으면, CI가 알아서 빨간불을 켜줍니다.
예시 3: 마스킹 후 이중 검증 — "정말로 마스킹이 작동하고 있는가?"
여기서 한 가지 더 짚고 싶은 부분이 있습니다. 마스킹 규칙이 "존재"하는 것과 "실제로 작동"하는 것은 다른 문제입니다. 규칙은 있는데 적용 순서가 꼬여서 일부 레코드가 마스킹되지 않는 경우를 실제로 겪어봤거든요. 우리 팀에서는 마스킹 파이프라인 리팩토링 중에 이 문제가 발생했는데, 겉으로는 멀쩡해 보여서 3주나 지나서야 발견했습니다. 그래서 마스킹된 데이터를 다시 Presidio로 스캔하는 이중 검증이 필요합니다.
from presidio_analyzer import AnalyzerEngine
def validate_masking_completeness(
masked_db, masking_rules: dict, score_threshold: float = 0.7
) -> list[dict]:
"""마스킹된 DB에서 여전히 PII가 남아있는지 레코드 단위로 검증"""
analyzer = AnalyzerEngine()
violations = []
for table, columns in masking_rules.items():
for col in columns:
samples = masked_db.sample(table, col, n=1000)
for idx, value in enumerate(samples):
if value is None:
continue
results = analyzer.analyze(
text=str(value),
language="en",
score_threshold=score_threshold,
)
if results:
violations.append({
"table": table,
"column": col,
"row_index": idx,
"detected_pii": [r.entity_type for r in results],
})
return violations| 파라미터 | 설명 |
|---|---|
n=1000 |
전수 검사 대신 샘플링으로 CI 속도를 유지합니다 |
score_threshold=0.7 |
너무 낮으면 오탐이 폭증하고, 너무 높으면 미탐이 생깁니다. 0.7은 경험적으로 균형 잡힌 시작점입니다 |
| 레코드 단위 반복 | 샘플을 통째로 문자열 변환하면 레코드 경계가 사라져서, 어떤 레코드에서 PII가 탐지됐는지 추적이 안 됩니다. 반드시 건별로 분석하는 게 좋습니다 |
주의: 위 코드에서
language="en"으로 설정한 이유는 아래 예시 4에서 설명하겠지만, Presidio의 기본 NER 모델이 한국어를 지원하지 않기 때문입니다. 한국어 PII 탐지를 위해서는 별도 설정이 필요합니다.
이 스크립트를 CI의 마지막 단계에 넣어두면, "규칙은 있는데 마스킹이 안 먹히는" 유령 규칙 문제를 잡아낼 수 있습니다.
예시 4: 한국어 PII 커스텀 인식기 — Presidio 한국어 한계와 대응
여기서 솔직하게 짚고 넘어가야 할 부분이 있습니다. Presidio의 기본 NER 모델(spaCy의 en_core_web_lg)은 한국어를 지원하지 않습니다. language="ko"를 넘기더라도 NER 레이어는 사실상 작동하지 않고, 정규식 기반 인식기만 동작합니다. 처음에 이걸 모르고 language="ko"만 설정하면 한국어 이름까지 잡아줄 거라고 기대했다가, 테스트 결과를 보고 좀 당황했던 기억이 있습니다.
한국어 PII를 제대로 탐지하려면 두 가지가 필요합니다.
- 정규식 기반 커스텀 인식기: 주민등록번호, 전화번호 등 정형 패턴
- 한국어 NLP 모델 연동: 한국어 이름, 주소 등 비정형 텍스트 (spaCy 한국어 모델이나 별도 NLP 파이프라인 필요)
우선 정규식 기반으로 바로 적용 가능한 커스텀 인식기부터 보겠습니다.
from presidio_analyzer import AnalyzerEngine, PatternRecognizer, Pattern
# 주민등록번호 인식기 (하이픈 포함/미포함)
rrn_patterns = [
Pattern(
name="rrn_with_hyphen",
regex=r"\b\d{6}-[1-4]\d{6}\b",
score=0.9,
),
Pattern(
name="rrn_without_hyphen",
regex=r"\b\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])[1-4]\d{6}\b",
score=0.7,
),
]
rrn_recognizer = PatternRecognizer(
supported_entity="KR_RRN",
name="Korean RRN Recognizer",
patterns=rrn_patterns,
supported_language="en",
)
# 한국 전화번호 인식기
phone_recognizer = PatternRecognizer(
supported_entity="KR_PHONE",
name="Korean Phone Recognizer",
patterns=[
Pattern(
name="kr_mobile",
regex=r"\b01[016789]-?\d{3,4}-?\d{4}\b",
score=0.85,
)
],
supported_language="en",
)
# 기본 영문 NLP 엔진 위에 커스텀 인식기를 추가
analyzer = AnalyzerEngine()
analyzer.registry.add_recognizer(rrn_recognizer)
analyzer.registry.add_recognizer(phone_recognizer)
# 사용
results = analyzer.analyze(
text="주민번호는 900101-1234567이고 연락처는 010-9876-5432입니다.",
language="en",
entities=["KR_RRN", "KR_PHONE"],
)score 값이 다른 이유가 궁금하실 수 있는데, 하이픈 포함 패턴(900101-1234567)은 주민번호 형태가 명확해서 0.9로 높게 잡았고, 하이픈 없는 패턴(9001011234567)은 일반 숫자열과 혼동될 가능성이 있어 0.7로 낮춘 겁니다. 오탐 가능성이 높은 패턴일수록 score를 낮게 설정하면, score_threshold로 걸러낼 때 유연하게 조절할 수 있습니다.
왜
supported_language="en"인가요? Presidio에 한국어 NLP 엔진을 별도로 등록하지 않은 상태에서language="ko"로 호출하면 에러가 발생합니다. 정규식 기반 인식기는 언어와 무관하게 동작하므로, 기본 영문 엔진 위에 올려도 정상 작동합니다. 한국어 NER(이름, 주소 등 비정형 탐지)까지 지원하려면 spaCy 한국어 모델(ko_core_news_sm)이나 다른 NLP 파이프라인을NlpEngineProvider에 등록해야 합니다. 이 부분은 다음 글에서 상세히 다루겠습니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 사전 차단 | 배포 전에 마스킹 누락을 발견하여 데이터 유출 사고를 예방할 수 있습니다 |
| 규정 준수 자동화 | GDPR, HIPAA, EU AI Act 등 규제 요건 충족을 CI가 자동으로 보증합니다 |
| 감사 추적 | 모든 마스킹 검증 결과가 CI 로그에 남아 감사 증적(audit trail)으로 활용 가능합니다 |
| 스키마 드리프트 감지 | 새 컬럼이나 테이블 추가 시 마스킹 누락을 자동으로 탐지합니다 |
| 개발자 피드백 루프 단축 | pre-commit과 CI에서 조기에 피드백을 제공하여 코드 리뷰 부담이 줄어듭니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 오탐(False Positive) | 정상 데이터를 PII로 오인하여 불필요하게 빌드가 차단될 수 있습니다 | score_threshold를 0.7에서 시작해 팀 상황에 맞게 튜닝합니다. 우리 팀은 처음에 0.5로 뒀다가 오탐에 지쳐서 0.75로 올렸습니다 |
| 미탐(False Negative) | 비정형 PII(자유 텍스트 속 이름 등)를 놓칠 수 있습니다 | NER + 정규식 + 도메인 규칙의 다층 접근으로 커버리지를 확보합니다 |
| 한국어 한계 | Presidio 기본 NER은 한국어를 지원하지 않아, 정규식 기반 탐지만 동작합니다 | 커스텀 인식기 추가 + spaCy 한국어 모델 연동이 필요합니다 |
| CI 속도 저하 | 대규모 스키마 전수 스캔 시 파이프라인 시간이 늘어납니다 | 스키마 diff 기반 증분 스캔으로 변경 부분만 검증합니다 |
| 규칙 유지보수 비용 | 스키마와 함께 진화하지 않으면 규칙이 빠르게 무용지물이 됩니다 | 마스킹 규칙을 스키마 파일 옆에 두고 같은 PR에서 함께 변경합니다 |
| 재식별 위험 | 개별 필드 마스킹만으로는 준식별자 조합에 의한 재식별이 가능합니다 | k-anonymity 등 별도 검증 레이어를 추가합니다 |
용어 보충 — 준식별자(Quasi-identifier): 그 자체로는 개인을 특정할 수 없지만, 여러 개를 조합하면 식별이 가능해지는 정보를 말합니다. 예를 들어 생년월일 + 성별 + 우편번호를 조합하면 상당수의 개인을 특정할 수 있다는 연구 결과가 있습니다.
용어 보충 — k-anonymity: 데이터셋에서 어떤 레코드든 최소 k개의 동일한 준식별자 조합을 가진 레코드가 존재하도록 보장하는 프라이버시 모델입니다. k=5라면 특정 조합으로 검색했을 때 최소 5명이 나와야 하니, 한 사람을 콕 집어내기 어려워집니다.
실무에서 가장 흔한 실수
-
마스킹 규칙과 스키마를 별도로 관리하는 것 — 스키마는 마이그레이션으로 바뀌는데 마스킹 규칙은 위키에 있으면, 둘의 싱크가 깨지는 건 시간문제입니다. 같은 리포지토리, 같은 PR에서 함께 변경하는 것을 권장합니다.
-
score_threshold를 튜닝하지 않고 기본값으로 운영하는 것 — 기본값이 너무 낮으면 오탐에 지친 개발자들이 경고를 무시하기 시작하고, 너무 높으면 실제 PII를 놓칩니다. 초기에 일주일 정도--warn-only모드로 돌려보면서 적절한 임계값을 찾는 과정이 반드시 필요합니다. -
규칙의 "존재"만 확인하고 "작동"은 확인하지 않는 것 — 마스킹 규칙이 YAML에 있다고 해서 실제로 마스킹이 되고 있다는 보장은 없습니다. 예시 3에서 소개한 이중 검증을 꼭 포함하는 것이 좋습니다. 저도 이걸 빠뜨렸다가 3주간 마스킹이 안 된 데이터가 스테이징에 쌓인 적이 있습니다.
마치며
이 파이프라인의 핵심은 "사람의 기억력"을 시스템으로 대체하는 것이지만, 진짜 가치는 그 너머에 있습니다. 한번 구축해두면 스키마가 바뀔 때마다 알아서 검증이 돌아가니 팀 전체의 인지 부하가 줄어드는 걸 체감할 수 있고, 이 자동화가 성숙해지면 Data Catalog와 연동하여 새 테이블이 추가될 때 마스킹 규칙을 자동으로 제안하는 수준까지 발전시킬 수 있습니다.
지금 바로 시작해볼 수 있는 3단계입니다.
-
pre-commit hook 설치부터 —
pip install pre-commit후.pre-commit-config.yaml에 PII 탐지 hook을 추가하면, 오늘 당장 코드 내 PII 하드코딩을 차단할 수 있습니다. 가장 적은 노력으로 가장 빠른 효과를 볼 수 있는 출발점입니다. -
마스킹 규칙 YAML 작성 — 현재 DB 스키마를 기준으로
masking-rules.yaml을 작성합니다. 처음부터 전체를 커버할 필요는 없고, 가장 민감한 테이블부터 시작하면 됩니다.users테이블의email,phone,name,address,date_of_birth와payments테이블의card_number,billing_address같은 컬럼은 거의 항상 PII입니다. 이런 "확실한 것들"부터 잡고, 점진적으로 확장하면 부담이 적습니다. -
CI에 스키마 커버리지 검증 추가 — 위에서 소개한
scan_schema_coverage.py와 GitHub Actions 워크플로를 PR 트리거로 걸어두면 됩니다. 처음 2주는--warn-only플래그로 경고만 출력하다가, 팀이 익숙해지면--fail-on-uncovered로 전환하는 것을 권장합니다.
다음 글: 「Presidio 커스텀 NLP 엔진으로 한국어 PII 탐지 정확도 끌어올리기 — spaCy 한국어 모델 연동부터 이름·주소 인식까지」
참고 자료
- Microsoft Presidio — GitHub | 오픈소스 PII 탐지·익명화 프레임워크
- Presidio 공식 문서 | 분석기, 익명화기, 커스텀 인식기 가이드
- Presidio Research & Evaluation | PII 탐지 모델 평가 도구
- PII Masker (HydroXai) — GitHub | DeBERTa-v3 기반 고정밀 AI 탐지
- HoundDog.ai — CI Integrations | CI/CD 파이프라인 PII 스캐너 통합 가이드
- Checkmarx + HoundDog.ai PII Leak Detection | AppSec에 PII 유출 탐지를 통합한 사례
- Piiano — Application Data Leaks Detection | 오픈소스 프로젝트의 PII 유출 탐지 사례 분석
- pii-secret-check-hooks — GitHub | PII + 시크릿 검사 pre-commit hook
- PII Detection in Integration Testing | hoop.dev | 통합 테스트에서 PII 유출을 잡는 방법
- Top Open Source Sensitive Data Discovery Tools (2025) | Bytebase | 오픈소스 민감 데이터 탐지 도구 비교
- Cyber Defense Magazine — PII Leak Detection in Code | 코드 레벨 PII 유출 탐지의 중요성