[파이썬 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는 긴 객체 로그를 통제 가능한 크기로 유지해, 디버깅 속도와 운영 안정성을 동시에 높여 주는 도구다.
기억할 것:
- 축약 출력은 데이터가 아니라 “표현”만 바꾼다.
pprint와reprlib는 경쟁 관계가 아니라 용도 분리 관계다.- 축약은 보안이 아니다. 민감정보는 반드시 별도 마스킹해야 한다.
연습문제
reprlib.Repr()를 사용해maxstring=30,maxlist=4,maxlevel=2정책을 만들고, 중첩 딕셔너리 출력 결과를 확인해 보세요.safe_repr(obj)함수를 만들어dict에서token,password키를 먼저 마스킹한 뒤 축약 문자열을 반환해 보세요.- 같은 데이터에 대해
pformat(data)와reprlib.repr(data)를 각각 로그에 남긴다고 가정하고, 어떤 상황에서 어떤 방식이 더 유리한지 설명해 보세요.
이전 강의 정답
print()vspprint()결과 비교
>>> 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의 줄바꿈/들여쓰기 이점이 크게 드러납니다.
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)
설명: 운영 로그에는 원문 토큰 대신 마스킹된 값만 남겨야 합니다.
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가 유리합니다.
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 사용 모듈:
reprlib,logging,pprint - 재현 팁:
- 터미널 폭에 따라 문자열 줄바꿈 체감이 달라질 수 있음
- 실제 서비스 로그 샘플에 제한값(
maxstring,maxlist,maxlevel)을 적용해 튜닝 권장 - 운영 반영 전 민감정보 마스킹 테스트를 먼저 수행