8장. 보안 - 안전한 도구 실행
개인정보 마스킹 — 모델에게 준 것은 화면에 나올 수 있다
한 줄 요약
도구가 돌려준 값은 모델의 입력이 되고, 모델의 입력은 답변에 그대로 나올 수 있습니다. 그래서 개인정보는 모델에게 건너가기 전에, 도구 안에서 가립니다. 일부를 가려서 전달하는 것을 마스킹(masking) 이라고 합니다.
1. 모델은 비밀을 지켜 주는 상대가 아니다
본인 확인을 통과한 정상 조회라도 문제가 남습니다. 도구가 돌려준 값에 이름, 전화번호, 주소가 들어 있으면 모델은 그것을 답에 쓸 수 있습니다.
"개인정보를 답변에 쓰지 마세요"라고 프롬프트에 적을 수는 있습니다. 그러나 그것은 부탁입니다. 설계할 때는 이렇게 가정합니다.
모델에게 전달한 정보는 언젠가 화면에 나온다.
이 가정에서 출발하면 할 일이 분명해집니다. 화면에 나오면 안 되는 것은 모델에게 주지 않습니다.
2. 방법 하나 — 애초에 돌려주지 않는다
가장 확실한 방법은 반환값에 넣지 않는 것입니다. orders.csv에는 받는 사람(recipient)과 주소(address) 열이 있습니다. 도구가 돌려주는 값을 봅니다.
return {
"found": True, "order_id": r["order_id"],
"product_name": r["product_name"], "option": r["option"],
"quantity": r["quantity"], "order_amount": r["order_amount"],
"ordered_at": r["ordered_at"], "status": r["order_status"],
"courier": ..., "tracking_no": ..., "shipped_at": ..., "delivered_at": ...,
}
recipient와 address가 없습니다. 배송 상태를 안내하는 데 주소는 필요하지 않기 때문입니다. 필요한 열만 골라 돌려주는 것이 첫 번째 방어입니다.
3. 방법 둘 — 보여 줘야 한다면 가려서 준다
"고객님"이라고 부르기 위해 이름이 필요한 경우처럼, 아예 빼기 어려운 정보도 있습니다. 그럴 때는 일부를 가립니다.
def mask_name(name: str) -> str:
"""김민준 → 김*준"""
if len(name) <= 1:
return name
return name[0] + "*" * (len(name) - 2) + name[-1] if len(name) > 2 else name[0] + "*"
def mask_phone(phone: str) -> str:
"""010-1234-5678 → 010-****-5678"""
parts = phone.split("-")
if len(parts) == 3:
return f"{parts[0]}-****-{parts[2]}"
return "****"
| 함수 | 입력 | 출력 | 규칙 |
|---|---|---|---|
mask_name |
김민준 | 김*준 | 첫 글자와 끝 글자만 남긴다 |
mask_name |
이솔 | 이* | 두 글자면 첫 글자만 남긴다 |
mask_phone |
010-1234-5678 | 010-****-5678 | 가운데 자리를 가린다 |
mask_phone |
01012345678 | **** | 형식이 다르면 전부 가린다 |
마지막 줄을 눈여겨봅니다. 예상한 형식(- 두 개)이 아니면 원본을 그대로 내보내지 않고 전부 가립니다. 형식을 모를 때는 가리는 쪽으로 넘어지게 만든 것입니다.
4. 어디서 가리는가
마스킹은 모델에게 건너가기 전, 곧 도구가 값을 돌려주는 자리에서 합니다.
def get_my_membership() -> dict:
return {"customer_id": session.customer_id,
"name": mask_name(session.name), # 가린 이름만 모델에게 간다
"grade": session.grade, "points": session.points}
C001로 로그인해 이 도구를 직접 부르면 이렇게 나옵니다.
{'customer_id': 'C001', 'name': '김*준', 'grade': '골드', 'points': 8420}
모델은 "김민준"이라는 원래 이름을 받은 적이 없습니다. 받지 않은 것은 답에 쓸 수 없습니다.
| 가리는 자리 | 성격 |
|---|---|
| 시스템 프롬프트에 "이름을 다 쓰지 마라" | 부탁. 모델은 원본을 알고 있다 |
| 모델의 답을 받은 뒤 코드로 지운다 | 답에 어떤 모양으로 나올지 몰라 빠뜨리기 쉽다 |
| 도구가 돌려줄 때 가린다 | 모델이 원본을 아예 모른다 |
5. 답변만이 출구가 아니다
정보가 사람 눈에 닿는 길은 답변 말고도 있습니다.
로그. 세션 객체를 print하면 무엇이 나올까요.
def __repr__(self):
return f"CustomerSession({self.customer_id}, {mask_name(self.name)})"
■ 로그인 세션: CustomerSession(C001, 김*준)
__repr__은 객체를 출력할 때의 모양을 정하는 파이썬의 약속된 이름입니다. 여기서도 이름을 가렸습니다. 개발하다 보면 객체를 화면이나 로그 파일에 찍게 되는데, 그 로그도 사람이 봅니다.
기록 파일. 이번 장에서 만드는 티켓에도 가린 이름만 적습니다.
"customer_name": mask_name(session.name), # 마스킹된 이름만 기록
핵심 정리
- 모델에게 준 정보는 답에 나올 수 있다고 가정하고 설계합니다.
- 가장 좋은 방법은 돌려주지 않는 것입니다. 주소와 받는 사람은 반환값에 없습니다.
- 꼭 필요한 정보는 가려서 줍니다. 형식을 모를 때는 전부 가립니다.
- 가리는 자리는 도구가 값을 돌려줄 때입니다. 그러면 모델은 원본을 알지 못합니다.
- 로그와 기록 파일도 사람이 보는 출구입니다. 거기에도 가린 값을 씁니다.