파이썬 실행 로그에 비밀값을 넣지 않기: 허용 필드형 JSON 이벤트 만들기

원본 응답을 로거에 넘기지 말고, 허용한 작업명·실행 ID·상태·처리 건수만 검증해 기록하세요.

적용 환경: Python 3.14 표준 라이브러리. 2026-10-23 확인. Windows Python 3.14.4에서 아래 합성 예제를 임시 작업 폴더로 실행해 assert와 예상 예외 검사를 통과했습니다. 실제 회사 파일·API·GUI는 실행하지 않았습니다.

문자열에서 비밀값을 지우기 전에 입력을 줄이세요

자동화 결과를 확인하려고 headers, 요청 URL, 응답 본문이나 설정 사전 전체를 출력하면 토큰·개인정보가 함께 기록될 수 있습니다. 이 글은 모든 패턴을 마스킹하는 필터 대신 기록할 데이터 자체를 적게 만드는 예제입니다. 원본 응답 객체를 함수가 받지 않게 하고, 결과 요약 이벤트만 새로 구성합니다. 이것만으로 프로그램의 모든 로그 경로가 안전해지는 것은 아닙니다.

허용 이벤트의 계약을 정의합니다

가상 작업명은 inventory_sync 또는 daily_report, 상태는 completed 또는 failed 중 하나입니다. processed는 bool이 아닌 0~1000000 정수이며, run_id는 영문 소문자·숫자·하이픈으로 된 1~40자 문자열입니다. 이 범위는 예제에서 정한 계약입니다. 정규식이 문자열의 비밀 여부를 판단하지는 못하므로 실행 ID에는 이메일·토큰을 재사용하지 말고 별도로 생성한 비민감 식별자를 넣어야 합니다.

직접 실행하는 최소 예제

import io
import json
import logging
import re

def make_event(*, job, run_id, status, processed):
    if job not in {"inventory_sync", "daily_report"}:
        raise ValueError("허용되지 않은 작업")
    if not isinstance(run_id, str) or re.fullmatch(r"[a-z0-9-]{1,40}", run_id) is None:
        raise ValueError("실행 ID 형식 오류")
    if status not in {"completed", "failed"}:
        raise ValueError("허용되지 않은 상태")
    if type(processed) is not int or not 0 <= processed <= 1000000:
        raise ValueError("처리 건수 범위 오류")
    return {"event": "job_result", "job": job, "run_id": run_id,
            "status": status, "processed": processed}

def expect_error(error, **kwargs):
    try:
        make_event(**kwargs)
    except error:
        return
    raise AssertionError("잘못된 입력이 통과했습니다")

valid = dict(job="inventory_sync", run_id="demo-20261023-01",
             status="completed", processed=12)
assert make_event(**valid)["processed"] == 12
assert make_event(**{**valid, "processed": 0})["processed"] == 0
for changes in [{"processed": True}, {"processed": -1}, {"processed": 1000001},
                {"run_id": "demo\nforged"}, {"job": "unknown"}, {"status": "raw error"}]:
    expect_error(ValueError, **{**valid, **changes})
# 임의 필드는 받지 않습니다. 실제 인증 정보는 사용하지 않습니다.
expect_error(TypeError, **valid, api_key="synthetic-secret")

stream = io.StringIO()
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter("%(message)s"))
logger = logging.getLogger("allowlist-demo")
logger.setLevel(logging.INFO)
logger.propagate = False
logger.addHandler(handler)
try:
    event = make_event(**valid)
    logger.info("%s", json.dumps(event, ensure_ascii=False, sort_keys=True))
    lines = stream.getvalue().splitlines()
    assert len(lines) == 1
    restored = json.loads(lines[0])
    assert restored == event
    assert set(restored) == {"event", "job", "run_id", "status", "processed"}
    assert "synthetic-secret" not in lines[0] and "api_key" not in lines[0]
    print(lines[0])
    print("허용 필드·자료형·범위·줄바꿈·추가 인자 거부 검사 통과")
finally:
    logger.removeHandler(handler)
    handler.close()

검증한 출력

{"event": "job_result", "job": "inventory_sync", "processed": 12, "run_id": "demo-20261023-01", "status": "completed"}
허용 필드·자료형·범위·줄바꿈·추가 인자 거부 검사 통과

추가 인자를 조용히 받아서 넘기지 않습니다

make_event는 키워드 인자를 네 개로 고정해 api_key 같은 임의 인자가 들어오면 TypeError를 냅니다. 함수 안에서 kwargs 전체를 로그에 남기는 경로를 만들지 않습니다. 필드값이 잘못돼 발생한 예외에도 원본 값을 포함하지 않는 고정 문구를 사용합니다. 예제의 synthetic-secret은 로컬 시험용 문자열이지 실제 인증 정보가 아닙니다.

JSON 한 줄을 다시 읽어 검증합니다

메모리 StreamHandler에 이벤트 한 줄을 기록한 뒤 json.loads로 역직렬화해 값과 필드 집합이 같은지 검사합니다. run_id의 줄바꿈을 거부해 이 필드에서 가짜 로그 행이 섞이지 않도록 제한했습니다. JSON 직렬화는 암호화나 비밀값 제거 기능이 아니므로 허용 필드에 비밀값을 넣으면 그대로 기록될 수 있습니다. 저장 형식을 바꾸는 것과 공개 범위를 줄이는 것은 다른 작업입니다.

예외 추적과 외부 라이브러리 로그는 별도로 점검하세요

logger.exception이나 exc_info=True의 traceback에는 예외 메시지가 추가될 수 있습니다. Requests 디버그 로그나 다른 모듈의 print 역시 make_event를 거치지 않습니다. 이 코드는 전역 마스킹 필터가 아니며 기존 로그 파일의 비밀값도 지우지 않습니다. 실패를 남길 때는 검토한 오류 코드처럼 제한된 분류를 별도 설계하고, 원본 오류·URL 전체를 상태 문자열에 넣지 마세요.

운영에서는 보관과 접근 제어도 필요합니다

예제는 파일·클라우드 전송 없이 메모리에서만 검사합니다. 독립 데모 로거는 상위 전달을 끄고 사용한 핸들러만 finally에서 제거합니다. 실제 앱의 로거 설정을 무작정 바꾸지 말고 기존 수집 경로·중복 핸들러·권한·회전·보관 기간을 확인하세요. 시각과 배포 버전이 필요하면 검증된 필드를 추가하고 테스트를 늘리되, 인증값이나 회사 응답 원문을 추가하는 방식으로 진단 범위를 넓히지 마세요.

자주 묻는 질문

JSON으로 바꾸면 토큰이 자동으로 가려지나요?

아닙니다. JSON은 직렬화 형식입니다. 어떤 필드를 넣는지와 값의 성격을 별도로 검토해야 합니다.

run_id 정규식을 통과하면 비밀값이 아닌가요?

아닙니다. 형태만 확인합니다. 토큰을 재사용하지 않고 비민감 실행 식별자를 생성한다는 운영 규칙이 필요합니다.

기존 로깅 코드에 붙이면 모두 보호되나요?

아닙니다. 이 함수로 만든 결과 이벤트만 다룹니다. 다른 로거, traceback, print와 외부 전송 경로는 별도로 검사해야 합니다.

공식 문서와 확인일

다음 공식 문서를 2026-10-23에 직접 열어 예제 동작과 대조했습니다. 입력 자료와 출력은 직접 작성한 합성 예제이며, 위 코드를 실행해 재현한 결과입니다.

관련 가이드와 다음 검사

작성 안내: AI 보조로 작성했으며 합성 자료의 로컬 실행 결과를 검증했습니다. 운영자의 실제 업무 경험이나 사람 전문가 검수를 주장하지 않습니다. 자신의 입력 계약과 환경에 맞는 테스트를 먼저 추가하세요.

댓글

이 블로그의 인기 게시물

API 키 저장 방법: 해싱과 암호화의 차이, Fernet 예제

파이썬 Google Sheets API 연동: 서비스 계정 권한과 읽기·쓰기

Pandas 결측치 처리: isna, fillna, dropna 선택 기준