맥락과 로그에서 비밀값 제외
80분 안팎
학습 목표
가상 토큰과 개인정보 fixture를 필터링하고 필요한 오류 정보만 보존합니다.
개념
로그는 오류를 돕지만 전달 경로도 됩니다
할 일 목록 조회 오류를 AI에게 설명하려고 요청 객체 전체를 복사하면 제목, 이메일, 인증 토큰이 같이 전달될 수 있습니다. 로그 파일에도 같은 값이 남으면 모델 입력만 고쳐서는 경로가 줄지 않습니다. 이번 레슨은 오류 조사에 필요한 필드를 새 객체로 선택하고, 남길 문자열 안의 가상 민감값을 치환합니다. 원본을 바꾸거나 실제 인증 정보를 시험하지 않습니다. 어떤 정보가 없어져도 오류를 재현할 수 있는지 먼저 판단하고 필요한 관찰을 보존하는 작업입니다.
가림보다 앞에서 수집 범위를 줄입니다
이번 브라우저 함수의 출력 필드는 taskId·operation·status·error 네 가지입니다. 각 필드는 원본에 존재하고 문자열일 때만 남깁니다. token·email·phone·title·headers·추가 객체는 복사하지 않습니다. 빠진 값을 null로 채우지 않으며 빈 객체는 빈 객체로 출력합니다. 허용 필드라는 이유만으로 그 값이 안전하다고 보장할 수는 없으므로 선택한 문자열에도 치환 함수를 적용합니다. 오류 코드와 동작 이름은 재현에 필요하고 실제 제목은 이번 실패 fixture에 필요 없다는 판단을 계약으로 고정합니다.
브라우저의 JSON 입출력 약속
표준 입력은 JSON 객체 하나이고 표준 출력은 필터 결과 객체 하나입니다. solution은 키를 정렬해 JSON으로 출력하며 한글은 그대로 유지합니다. 입력 문법 자체가 잘못된 경우는 채점 범위 밖입니다. 중첩된 임의 구조를 재귀 탐색하여 일부만 가리는 과제도 아닙니다. 허용된 최상위 문자열 필드만 새로 구성하므로 임의 추가 구조는 통째로 제외합니다. 숫자 taskId를 자동 문자열로 바꾸지 않고 생략하는 선택도 명시된 계약입니다. 정답을 맞추려고 임의 원본 필드를 되살리지 않습니다.
가상 토큰의 치환 범위를 고정합니다
정규식 DEMO_TOKEN_[A-Z0-9_]+와 일치하는 부분을 [TOKEN]으로 치환합니다. 실습 토큰은 이름 자체가 가상 값임을 드러내며 실제 키를 수집하지 않습니다. 소문자가 섞인 임의 키, 인코딩된 키, 다른 형식의 인증값까지 자동 탐지한다는 뜻은 아닙니다. 규칙에서 약속한 토큰 형식과 최대한 다른 모양의 값을 시험해 검출 한계를 이해할 수 있습니다. 출력에서 실제 토큰을 발견했을 때에는 필터를 보완하는 것과 이미 전달된 값을 대응하는 일을 별개로 다루어야 합니다.
이메일과 전화번호도 fixture 계약입니다
ASCII 이메일 패턴은 영문자·숫자와 일부 기호, @, 점으로 구분된 도메인 및 영문 접미사를 대상으로 합니다. 전화번호는 010-숫자 네 자리-숫자 네 자리 형식을 [PHONE]으로 바꿉니다. 모든 국제 번호나 한글 이메일을 인식하는 개인정보 탐지기가 아닙니다. student@example.invalid와 010-1234-5678은 제공된 연습 자료이며 자신의 연락처로 바꾸지 않습니다. 이 제한을 prompt와 인계 메모에 적어야 테스트 통과가 모든 민감정보 제거의 보증으로 확대되지 않습니다.
원인을 설명할 정보는 보존합니다
NOT_FOUND라는 오류 코드, list라는 작업, failed라는 상태가 남아야 목록 실패를 이해할 수 있습니다. 전체 오류 문장을 빈 문자열로 바꾸면 토큰은 없어져도 원인을 조사할 수 없습니다. error 안에 오류 코드와 민감값이 섞였다면 코드와 주변 설명을 남기고 민감 부분만 치환합니다. 예외의 클래스나 고정된 검증 ID를 남기는 방식도 검토할 수 있습니다. 원본 제목 없이도 같은 실패를 재현하는 합성 입력을 만들어 전달하면 필요한 자료와 개인 내용의 분리가 더 분명해집니다.
필터는 맥락과 로그 양쪽에 적용합니다
미션의 context는 모델로 넘길 객체를 새로 만들고 clean_text는 실행 로그를 저장하기 전에 적용합니다. m07의 capture에 출력 필터 인수를 추가하여 stdout과 stderr를 합친 뒤 가린 문자열을 기록합니다. 원문을 파일에 저장하고 다시 열어 지우는 방식은 그 사이에 원문이 남기 때문에 사용하지 않습니다. 필터가 추가되어도 종료 코드와 검사 시각은 실제 관찰을 유지합니다. 토큰이 가려졌다는 사실만으로 실패 명령을 성공 처리하지 않습니다.
로그 해시는 가린 내용의 해시입니다
저장한 evidence 로그에서 민감 부분이 치환되면 m07 때의 원문 바이트와 달라집니다. logHash는 실제 저장한 가린 로그에서 계산해야 재개 검사와 일치합니다. 해시를 원문에서 계산한 채 가린 파일을 저장하면 파일 변조처럼 재검증 대상으로 보일 수 있습니다. 미션 테스트는 실제 로그 파일을 읽어 가림 문자열과 해시의 일치를 함께 확인합니다. 토큰을 그대로 해시로 바꿔 공개하는 것을 익명화 보장이라고 설명하지 않습니다. 내용 선택과 기록의 일관성이 이번 목적입니다.
원본을 수정하지 않는 이유
원본 객체에서 token 키를 삭제하면 호출한 쪽이 나중에 필요한 값까지 잃을 수 있습니다. 필터는 별도 객체를 반환하고 원본은 실습 메모리 안에 유지합니다. 원본을 그대로 찍는 디버깅 문장은 사용하지 않습니다. 한 번 가린 결과에 같은 필터를 다시 적용해도 값이 더 달라지지 않는지 검사합니다. 이를 멱등성이라고 부르며 [TOKEN]이나 [EMAIL]이 다시 변하지 않는 간단한 사례로 확인할 수 있습니다. 원본을 보존한다는 말이 원본을 장기 로그에 보관하라는 권장은 아닙니다.
정규식 치환 순서를 읽습니다
가상 토큰, 이메일, 전화번호 순으로 re.sub를 수행합니다. 각 패턴은 문자열 전체가 아니라 일치한 부분을 바꾸므로 error 안의 오류 코드와 공백은 유지됩니다. replace로 특정 fixture 한 값만 바꾸면 다른 DEMO_TOKEN_A나 다른 이메일에서 실패합니다. 패턴이 너무 넓으면 정상 오류 코드까지 지울 수 있습니다. 테스트에 민감값 두 개와 정상 오류가 섞인 사례를 넣어 삭제 범위를 확인합니다. 치환 이름만 맞추고 주변 문장이 달라지는 버그도 출력 비교에서 확인합니다.
오류 메시지와 채점 실패를 구별합니다
JSONDecodeError가 나면 필터 논리보다 입력 형식과 따옴표를 먼저 확인합니다. 기대 객체에 없는 token이 남으면 원본 전체 복사를 제거했는지 봅니다. 기대 [EMAIL] 대신 원문이 남으면 패턴과 re.sub 호출을 확인합니다. error가 통째로 사라졌다면 허용 필드 목록에서 빠졌거나 문자열 검사 순서가 잘못되었을 수 있습니다. 테스트 출력에는 디버깅 문장이나 가림 전 원문을 붙이지 않습니다. 조사 중에도 민감값 대신 어떤 패턴이 실패했는지를 표시하는 방식으로 기록합니다.
검증 결과의 범위를 인계합니다
정상 오류만 있는 객체, 민감 필드만 있는 객체, 허용 필드 안에 가상 비밀값이 있는 객체, 빈 객체, 숫자 필드, 반복 치환을 확인합니다. 통과 메모에는 네 필드 선택과 세 fixture 형식 치환을 검증했다고 씁니다. 실제 서비스의 모든 비밀이나 개인정보를 제거했다고 쓰지 않습니다. 제목이 필요한 조사라면 그대로 포함하기보다 합성 제목으로 같은 오류를 만드는 방법부터 찾습니다. 생성 코드의 일반 보안 검토는 더 읽기로 보내고 이번 제출은 선택 필드와 가린 값의 정확한 JSON을 완성합니다.
따라하기
필드를 선택한 새 객체를 만듭니다
가상 민감 필드를 원본에서 지우지 않고 전달 대상에서 제외합니다.
record = {'taskId':'T-8', 'status':'failed', 'token':'DEMO_TOKEN_ONLY'}
selected = {k: record[k] for k in ('taskId', 'status') if k in record}
print(selected)
print('원본 token 유지:', 'token' in record)
실행 결과
{'taskId': 'T-8', 'status': 'failed'}
원본 token 유지: True
오류 설명의 민감 부분을 가립니다
세 가지 fixture 형식만 치환하며 오류 코드는 유지합니다.
import json, re, sys
def clean_text(text):
text = re.sub(r'DEMO_TOKEN_[A-Z0-9_]+', '[TOKEN]', text)
text = re.sub(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', '[EMAIL]', text)
return re.sub(r'010-\d{4}-\d{4}', '[PHONE]', text)
def context(record):
result = {}
for key in ('taskId', 'operation', 'status', 'error'):
if key in record and isinstance(record[key], str):
result[key] = clean_text(record[key])
return result
print(clean_text('NOT_FOUND DEMO_TOKEN_ONLY student@example.invalid 010-1234-5678'))
실행 결과
NOT_FOUND [TOKEN] [EMAIL] [PHONE]
반복 적용이 안정적인지 확인합니다
이미 가린 표현이 다시 변하지 않는지 확인합니다.
import json, re, sys
def clean_text(text):
text = re.sub(r'DEMO_TOKEN_[A-Z0-9_]+', '[TOKEN]', text)
text = re.sub(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', '[EMAIL]', text)
return re.sub(r'010-\d{4}-\d{4}', '[PHONE]', text)
def context(record):
result = {}
for key in ('taskId', 'operation', 'status', 'error'):
if key in record and isinstance(record[key], str):
result[key] = clean_text(record[key])
return result
once = clean_text('DEMO_TOKEN_A student@example.invalid')
print(once)
print('반복 결과 동일:', once == clean_text(once))
실행 결과
[TOKEN] [EMAIL] 반복 결과 동일: True
비문자열과 추가 객체를 제외합니다
계약에 없는 중첩 필드는 전달하지 않고 문자열 오류만 남깁니다.
import json, re, sys
def clean_text(text):
text = re.sub(r'DEMO_TOKEN_[A-Z0-9_]+', '[TOKEN]', text)
text = re.sub(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', '[EMAIL]', text)
return re.sub(r'010-\d{4}-\d{4}', '[PHONE]', text)
def context(record):
result = {}
for key in ('taskId', 'operation', 'status', 'error'):
if key in record and isinstance(record[key], str):
result[key] = clean_text(record[key])
return result
value = context({'taskId':8, 'error':'NOT_FOUND', 'headers':{'token':'DEMO_TOKEN_ONLY'}})
print(json.dumps(value, sort_keys=True))
실행 결과
{"error": "NOT_FOUND"}
확인 문제
실습
JSON 객체에서 문자열인 taskId·operation·status·error만 새 객체에 남깁니다. 없는 필드·비문자열·추가 필드·중첩 객체는 생략합니다. 남길 문자열에서 DEMO_TOKEN_[A-Z0-9_]+는 [TOKEN], ASCII 이메일 패턴 [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}는 [EMAIL], 010-숫자4자리-숫자4자리는 [PHONE]으로 치환합니다. 오류 코드와 주변 문장은 유지하고 결과 JSON 하나만 출력합니다. 빈 문자열은 보존합니다. 원본은 변경하지 않습니다. 실제 키·모든 이메일·모든 전화번호 탐지를 보장하는 과제가 아닙니다.
모범 답안
import json, re, sys
def clean_text(text):
text = re.sub(r'DEMO_TOKEN_[A-Z0-9_]+', '[TOKEN]', text)
text = re.sub(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}', '[EMAIL]', text)
return re.sub(r'010-\d{4}-\d{4}', '[PHONE]', text)
def context(record):
result = {}
for key in ('taskId', 'operation', 'status', 'error'):
if key in record and isinstance(record[key], str):
result[key] = clean_text(record[key])
return result
print(json.dumps(context(json.load(sys.stdin)), ensure_ascii=False, sort_keys=True))
더 읽기
면접 질문
- AI가 만든 코드가 실행될 때 추가로 확인할 내용을 설명해 주시면 됩니다.