[파이썬 100강] 85강. reprlib로 긴 객체 출력을 안전하게 축약해 로그 가독성 높이기

[파이썬 100강] 85강. reprlib로 긴 객체 출력을 안전하게 축약해 로그 가독성 높이기

디버깅할 때 객체를 그대로 출력하면 금방 로그가 폭발합니다. 특히 긴 문자열, 대량 리스트, 깊은 중첩 구조를 print()logging에 그대로 넣으면 핵심은 안 보이고 비용만 커지는 상황이 자주 생깁니다. 이번 강의에서는 표준 라이브러리 reprlib를 사용해 “필요한 만큼만” 안전하게 보여 주는 방법을 바로 예제로 익힙니다.


핵심 개념

  • reprlib.repr(obj)는 객체의 표현을 길이 제한과 깊이 제한을 적용해 축약 문자열로 반환합니다.
  • reprlib.Repr 인스턴스를 만들어 문자열 길이(maxstring), 리스트 길이(maxlist), 깊이(maxlevel) 등을 세밀하게 조정할 수 있습니다.
  • 축약 출력은 데이터 자체를 바꾸지 않고, 보여 주는 방식만 바꾸는 접근입니다.
  • 운영 로그에서는 “전체 덤프”보다 “핵심 컨텍스트 + 축약 표현”이 더 안전하고 읽기 쉽습니다.
  • pprint가 구조를 예쁘게 펴 주는 도구라면, reprlib너무 긴 출력을 통제하는 도구라고 보면 됩니다.

실무에서 장애를 추적할 때 가장 흔한 병목 중 하나가 “로그는 많은데 읽을 수 없다”는 문제입니다. 긴 payload가 한 번에 찍히면 검색도 어려워지고, 저장비도 늘고, 중요한 오류 라인이 묻혀 버립니다. 이때 reprlib를 적용하면 문자열/시퀀스/중첩 레벨을 정책적으로 제한해 로그를 짧고 일관되게 유지할 수 있습니다.

중요한 포인트는 축약이 곧 손실이라는 점입니다. 그래서 운영 패턴은 보통 이렇게 갑니다. 기본 로그는 축약, 필요 시 샘플/추적 모드에서 원문 저장. 즉 항상 모든 걸 다 찍는 게 아니라, 읽을 수 있는 기본 로그를 중심으로 설계하고, 깊은 분석이 필요한 케이스만 확장하는 전략이 유지보수에 유리합니다.


기본 사용

예제 1) 기본 reprlib.repr()로 긴 리스트/문자열 줄이기

>>> import reprlib
>>> long_text = "A" * 120
>>> long_list = list(range(30))
>>> reprlib.repr(long_text)
"'AAAAAAAAAAAAAAAAAAAAAAAAAAAA...AAAAAAAAAAAAAAAAAAAAAAAAAAAAA'"
>>> reprlib.repr(long_list)
'[0, 1, 2, 3, 4, 5, ...]'

해설:

  • 긴 데이터가 자동으로 ... 형태로 줄어듭니다.
  • 출력은 짧아지지만 원본 변수의 데이터는 그대로 유지됩니다.
  • “대략 어떤 값인지” 빠르게 확인할 때 매우 유용합니다.

예제 2) Repr 인스턴스로 정책 튜닝하기

>>> import reprlib
>>> r = reprlib.Repr()
>>> r.maxstring = 20
>>> r.maxlist = 5
>>> r.maxlevel = 3
>>> payload = {
...     "user": "geonwoo-" + "x" * 30,
...     "items": [{"id": i, "name": f"item-{i}"} for i in range(10)],
...     "meta": {"trace": {"step": [1, 2, 3, 4, 5, 6]}}
... }
>>> r.repr(payload)
"{'items': [{'id': 0, 'name': 'item-0'}, {'id': 1, 'name': 'item-1'}, {'id': 2, 'name': 'item-2'}, {'id': 3, 'name': 'item-3'}, {'id': 4, 'name': 'item-4'}, ...], 'meta': {'trace': {'step': [...]}}, 'user': 'geonwoo-xxxx...xxxxxxxxxxxxx'}"

해설:

  • reprlib.Repr()를 쓰면 팀 로그 정책(최대 길이, 최대 원소 수)을 코드로 고정할 수 있습니다.
  • 특히 maxlevel은 중첩 객체에서 로그 폭증을 막는 데 큰 역할을 합니다.

예제 3) 로깅 함수에 축약 표현 적용하기

>>> import logging, reprlib
>>> logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
>>> short = reprlib.Repr()
>>> short.maxstring = 40
>>> short.maxlist = 6
>>>
>>> def safe_debug(label, obj):
...     logging.info("%s=%s", label, short.repr(obj))
...
>>> event = {
...     "request_id": "req-20260217-1300",
...     "token": "sk-" + "abc123" * 20,
...     "rows": [{"idx": i, "value": i * 10} for i in range(20)]
... }
>>> safe_debug("event", event)
INFO event={'request_id': 'req-20260217-1300', 'rows': [{'idx': 0, 'value': 0}, {'idx': 1, 'value': 10}, {'idx': 2, 'value': 20}, {'idx': 3, 'value': 30}, {'idx': 4, 'value': 40}, {'idx': 5, 'value': 50}, ...], 'token': 'sk-abc123abc123a...c123abc123abc123'}

해설:

  • 로거에 원본 객체를 직접 넣지 말고, 축약 정책을 거쳐 일관된 문자열로 남기면 분석이 쉬워집니다.
  • 여기에 민감정보 마스킹을 결합하면 운영 안전성이 훨씬 올라갑니다.

예제 4) pprint와 역할 분리하기

>>> from pprint import pformat
>>> import reprlib
>>> data = {"items": [{"id": i, "name": "N" * 20} for i in range(12)]}
>>> pretty = pformat(data, width=60)
>>> short = reprlib.repr(data)
>>> len(pretty) > len(short)
True

해설:

  • pprint는 보기 좋게 “펼치기”, reprlib는 길이를 “줄이기”가 핵심입니다.
  • 디버깅 단계에서는 pprint, 운영 로그 기본값은 reprlib처럼 용도 분리하면 좋습니다.

자주 하는 실수

실수 1) 축약된 문자열을 원본 데이터처럼 다시 파싱하려고 함

>>> import reprlib, json
>>> obj = {"nums": list(range(100))}
>>> s = reprlib.repr(obj)
>>> json.loads(s)
Traceback (most recent call last):
... json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes ...

원인:

  • reprlib.repr() 결과는 사람이 읽기 위한 “표현 문자열”이지, JSON 직렬화 포맷이 아닙니다.
  • 게다가 ...로 축약되어 있으므로 정보가 의도적으로 생략되어 있습니다.

해결:

>>> import json
>>> json.dumps(obj, ensure_ascii=False)[:60]
'{"nums": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,'
  • 전송/저장은 json.dumps, 디버깅 가독성은 reprlib처럼 목적을 분리합니다.

실수 2) 민감정보가 축약되면 안전하다고 오해함

  • 증상: 토큰, 이메일, 전화번호 일부가 로그에 노출
  • 원인: 축약은 길이만 줄일 뿐, 민감정보를 자동 마스킹하지 않음
  • 해결:
    • 축약 전에 마스킹 함수 적용 (token -> "***", email -> 앞 2글자 + ***)
    • 민감 필드는 아예 로그 제외하는 allowlist 방식 채택
    • 운영/개발 로그 정책을 분리하고 코드 리뷰 시 체크리스트에 포함

실수 3) maxlist, maxstring을 너무 작게 잡아 디버깅 정보를 잃어버림

>>> import reprlib
>>> r = reprlib.Repr(); r.maxlist = 1; r.maxstring = 8
>>> r.repr({"errors": ["timeout", "auth", "db"], "msg": "payment-failed-critical"})
"{'errors': ['timeout', ...], 'msg': 'pay...cal'}"

원인:

  • 로그 비용을 줄이겠다고 제한값을 과도하게 낮추면 핵심 단서도 같이 사라집니다.

해결:

>>> r.maxlist = 6; r.maxstring = 40
>>> r.repr({"errors": ["timeout", "auth", "db"], "msg": "payment-failed-critical"})
"{'errors': ['timeout', 'auth', 'db'], 'msg': 'payment-failed-critical'}"
  • 서비스 성격에 맞는 적정값을 정하고, 실제 장애 분석 케이스로 튜닝합니다.

실무 패턴

  • 입력 검증 규칙

    • 로그에 넣기 전, 객체 크기(리스트 길이/문자열 길이/중첩 깊이)를 확인합니다.
    • dict 전체를 덤프하지 말고 request_id, user_id, status 같은 핵심 키만 추립니다.
  • 로그/예외 처리 규칙

    • 예외 핸들러에서 reprlib 정책 객체를 공통 사용해 로그 포맷을 통일합니다.
    • logger.exception("checkout failed: %s", short.repr(context)) 패턴으로 스택트레이스+축약 컨텍스트를 같이 남깁니다.
  • 재사용 함수/구조화 팁

    • safe_repr(obj) 유틸 함수를 만들어 팀 전체가 같은 제한값을 사용하게 합니다.
    • 서비스별 정책 파일(예: API 서버는 maxstring=120, 배치 작업은 maxlist=20)로 분리하면 운영 유연성이 좋아집니다.
  • 성능/메모리 체크 포인트

    • 고빈도 루프에서 매번 reprlib.repr() 호출하면 비용이 쌓일 수 있으니, 에러 케이스나 샘플링 로깅에 집중합니다.
    • 대용량 객체는 축약하더라도 생성 자체가 비쌀 수 있으므로, “아예 필요한 필드만 추출”이 최우선입니다.

오늘의 결론

한 줄 요약: reprlib는 긴 객체 로그를 통제 가능한 크기로 유지해, 디버깅 속도와 운영 안정성을 동시에 높여 주는 도구다.

기억할 것:

  • 축약 출력은 데이터가 아니라 “표현”만 바꾼다.
  • pprintreprlib는 경쟁 관계가 아니라 용도 분리 관계다.
  • 축약은 보안이 아니다. 민감정보는 반드시 별도 마스킹해야 한다.

연습문제

  1. reprlib.Repr()를 사용해 maxstring=30, maxlist=4, maxlevel=2 정책을 만들고, 중첩 딕셔너리 출력 결과를 확인해 보세요.
  2. safe_repr(obj) 함수를 만들어 dict에서 token, password 키를 먼저 마스킹한 뒤 축약 문자열을 반환해 보세요.
  3. 같은 데이터에 대해 pformat(data)reprlib.repr(data)를 각각 로그에 남긴다고 가정하고, 어떤 상황에서 어떤 방식이 더 유리한지 설명해 보세요.

이전 강의 정답

  1. print() vs pprint() 결과 비교
>>> from pprint import pprint
>>> sample = {"a": [{"x": 1, "y": [10, 20, 30]}, {"x": 2, "y": [40, 50]}]}
>>> print(sample)
{'a': [{'x': 1, 'y': [10, 20, 30]}, {'x': 2, 'y': [40, 50]}]}
>>> pprint(sample)
{'a': [{'x': 1, 'y': [10, 20, 30]}, {'x': 2, 'y': [40, 50]}]}

설명: 이 예시는 구조가 짧아 차이가 작지만, 항목이 많아질수록 pprint의 줄바꿈/들여쓰기 이점이 크게 드러납니다.

  1. token 마스킹 후 pformat() 문자열 저장
>>> from pprint import pformat
>>> data = {"user": "geonwoo", "token": "abcd-very-secret-token", "ok": True}
>>> safe = {**data, "token": "***"}
>>> text = pformat(safe, width=60)
>>> "token" in text, "***" in text
(True, True)

설명: 운영 로그에는 원문 토큰 대신 마스킹된 값만 남겨야 합니다.

  1. sort_dicts 차이 확인
>>> from pprint import pprint
>>> cfg = {"z": 1, "a": 2, "m": 3}
>>> pprint(cfg, sort_dicts=True)
{'a': 2, 'm': 3, 'z': 1}
>>> pprint(cfg, sort_dicts=False)
{'z': 1, 'a': 2, 'm': 3}

설명: 입력 순서 자체가 의미라면 sort_dicts=False가 더 읽기 좋고, 키 이름 기준으로 빠르게 찾고 싶다면 True가 유리합니다.


실습 환경/재현 정보

  • 실행 환경: conda env python100 (Python 3.11.14)
  • 가정한 OS: macOS/Linux 공통
  • 사용 모듈: reprlib, logging, pprint
  • 재현 팁:
    • 터미널 폭에 따라 문자열 줄바꿈 체감이 달라질 수 있음
    • 실제 서비스 로그 샘플에 제한값(maxstring, maxlist, maxlevel)을 적용해 튜닝 권장
    • 운영 반영 전 민감정보 마스킹 테스트를 먼저 수행