外に出せる一枚をつくる
한국어 원문으로 표시합니다.
목표
개인정보가 섞인 고객 데이터에서 조사를 끝내고, 밖으로 나가도 되는 한 장을 만듭니다. 네 원칙(최소 수집 · 가명화 · 집계 반출 · 기록)을 각각 파일로 남깁니다.
환경
/root/pii 아래에서 작업합니다. python3, openssl, jq 가 들어 있습니다.
원본은 직접 만듭니다 — 아래는 준비이지 과제가 아닙니다.
mkdir -p /root/pii && cd /root/pii
python3 - <<'PY'
import csv, random
random.seed(7)
names = ['김민준','이서연','박지호','최수아','정하윤','강도윤','조서준','윤지우']
codes = ['TIMEOUT','DECLINED','INVALID_CARD','NETWORK','OK']
rows = []
for i in range(1, 201):
n = random.choice(names)
rows.append({
'order_id': 'ORD-%04d' % i,
'created_at': '2026-09-%02dT%02d:%02d:00Z' % (
random.randint(1, 7), random.randint(0, 23), random.randint(0, 59)),
'name': n,
'email': 'user%03d@example.com' % i,
'phone': '010-%04d-%04d' % (random.randint(1000, 9999), random.randint(1000, 9999)),
'address': '서울시 어딘가 %d길 %d' % (random.randint(1, 90), random.randint(1, 300)),
'card_last4': '%04d' % random.randint(1000, 9999),
'amount': random.randint(1000, 90000),
'status': random.choice(['FAILED', 'FAILED', 'PAID']),
'error_code': random.choice(codes),
})
with open('orders.csv', 'w', newline='', encoding='utf-8') as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
print(len(rows), '행')
PY
조사 목적은 하나입니다 — 결제 실패가 어떤 사유로 나는지 알아내는 것. 이름과 주소는 그 목적에 필요하지 않습니다.
만들 것
slim.csv 조사에 필요한 열만 남긴 사본
pseudo.sh 같은 사람을 묶어야 할 때 쓰는 가명값 생성기
masked.csv 육안 확인용 부분 마스킹본
report.csv 밖으로 나가는 유일한 파일 — 사유별 집계
scan.sh 반출 전 검사기
access.log 무엇을 왜 봤는지
handoff.md 무엇을 내보내고 무엇을 안 내보내는지, 그 근거
채점 방식
3단계와 6단계는 채점기가 여러분의 스크립트를 직접 돌립니다.
pseudo.sh 같은 값(대소문자·공백만 다름) → 같은 결과여야 한다
키가 다르면 → 다른 결과여야 한다
출력 길이 → 32자 이상이어야 한다
scan.sh 이메일이 든 파일 → 막아야 한다
전화번호가 든 파일 → 막아야 한다
집계만 든 파일 → 통과시켜야 한다
단계
- 원본을 만듭니다(위 준비 블록).
slim.csv— 조사에 필요한 열만 남깁니다. 행 수는 그대로입니다.pseudo.sh— 값 하나를 받아 가명값 한 줄을 냅니다. 키는PII_KEY환경변수로 받습니다.masked.csv— 전화번호를010-****-1234꼴로, 이메일도 가립니다.report.csv— 사유별 건수. 합계가 원본 행 수와 같아야 합니다.scan.sh— 파일 하나를 받아 개인정보가 있으면 0 이 아닌 코드로 끝냅니다.access.log— 조회할 때마다 시각 · 대상 · 목적을 한 줄씩.handoff.md— 무엇을 내보내고 무엇을 안 내보내는지, 마스킹 방식을 왜 그렇게 골랐는지.
참고
3단계에서 흔히 물리는 자리가 셋입니다. 정규화 없이 해시하면 대소문자만 달라도 같은 사람이 안 묶입니다. 키 없이 해시하면 전화번호나 이메일은 후보를 만들어 전수 대입할 수 있습니다. 그리고 8자로 잘라 쓰면 충돌이 납니다. 채점기가 셋을 따로 확인하니 어느 것이 걸렸는지 바로 알 수 있습니다.
7단계의 access.log 에도 개인정보를 적지 마세요. 감사 로그도 유출 경로입니다.
개인정보가 섞인 원본
원본을 만듭니다(위 준비 블록).
지시문의 준비 블록을 그대로 돌립니다. 조사에 필요한 열과 개인정보 열이 함께 들어 있어야 골라내는 연습이 됩니다.
필요한 열만 가져온다
slim.csv — 조사에 필요한 열만 남깁니다. 행 수는 그대로입니다.
SELECT * 대신 조사에 쓸 열만 고릅니다. 결제 실패 사유를 보는 데 이름과 주소는 필요하지 않습니다. 행은 줄이지 말고 열만 줄이세요 — 줄이다가 못 쓰게 만들면 안 됩니다.
같은 사람을 묶어야 할 때
pseudo.sh — 값 하나를 받아 가명값 한 줄을 냅니다. 키는 PII_KEY
환경변수로 받습니다.
openssl dgst -sha256 -hmac "$PII_KEY" -r 을 씁니다. 해시하기 전에 소문자로 바꾸고 공백을 없애야(정규화) 같은 사람이 같은 값으로 묶입니다. 출력은 자르지 마세요.
육안 확인용 부분 마스킹
masked.csv — 전화번호를 010-****-1234 꼴로, 이메일도 가립니다.
전화번호는 뒤 네 자리만 남깁니다(010-****-1234). 되돌릴 수는 없지만 유추는 가능하므로, 사람이 눈으로 대조해야 할 때만 씁니다.
밖으로 나가는 것은 집계뿐
report.csv — 사유별 건수. 합계가 원본 행 수와 같아야 합니다.
원본 200행 대신 '사유별 건수' 표 한 장입니다. 합계가 원본 행 수와 같아야 빠진 것이 없다고 말할 수 있습니다.
반출 전 검사기
scan.sh — 파일 하나를 받아 개인정보가 있으면 0 이 아닌 코드로 끝냅니다.
파일 하나를 받아 개인정보가 있으면 0 이 아닌 코드로 끝냅니다. 전부 막는 것도 실패입니다 — 집계만 든 파일은 통과시켜야 합니다. 오탐이 나면 아무도 안 씁니다.
무엇을 왜 봤는지 남긴다
access.log — 조회할 때마다 시각 · 대상 · 목적을 한 줄씩.
나중에 '누가 이걸 봤나' 라는 질문이 왔을 때 스스로를 방어하는 유일한 수단입니다. 목적이 빠지면 방어가 되지 않습니다. 그리고 이 파일에도 개인정보를 적지 마세요.
무엇을 내보내고 무엇을 안 내보내나
handoff.md — 무엇을 내보내고 무엇을 안 내보내는지, 마스킹 방식을 왜
그렇게 골랐는지.
마스킹 방식은 되돌릴 수 있는가로 나뉩니다. 각 방식을 왜 그 자리에 썼는지 적으세요. 이 문서 자체에도 개인정보가 들어가면 안 됩니다.