디버깅 로그가 서비스를 죽인다: `defaultdict.__repr__` 무한 재귀 버그가 보여준 ‘가드의 계약’ 운영하다 보면 가끔 이해가 안 되는 장애를 만난다. 기능을 바꾼 것도 아닌데 프로세스가 죽고, 원인을 쫓아가 보면 마지막 로그가 defaultdict(...) 같은 문자열 하나로 끝나 있다. “그냥 출력인데요?” repr은 그렇게 순한 기능이 아니다. repr은 ‘출력’이 아니라 실행 경로다. 로그를 남기려고 객체를 문자열로 바꾸는 순간, 그 객체의 코드가 실행된다. 그리고 그 코드가 재귀로 빠지면, 디버깅이 아니라 장애가 된다. 이번 주 CPython에 흥미로운 버그가 하나 정리됐다. collections.defaultdict.__repr__ 이 특정 조건에서 무한 재귀로 빠질 수 있었고, PR이 2026-03-10에 머지되면서 수정됐다(참고자료). 이 글은 CPython 내부를 공부하자는 글이 아니다. 내가 말하고 싶은 건 훨씬 단순하다. 재귀 가드가 있는 API는 enter/leave의 계약을 지키지 않으면, 디버깅이 서비스 안정성을 공격한다 는 것. 문제가 생기는 코드: 최소 재현은 ‘이상한’ 코드가 아니다 defaultdict 를 쓰는 건 흔하다. 없는 키를 조회할 때 기본값을 만들어 주는 게 편하니까. 여기서 트릭은 default_factory 다. 보통은 list 나 int 를 넣지만, 실무에서는 조금 더 복잡한 팩토리를 넣기도 한다. 예를 들어 로깅을 하고 싶어서, 혹은 상태를 캡처하고 싶어서, 팩토리 자체가 뭔가를 품고 있는 객체가 되기도 한다. 아래 코드는 “이상한 코드”처럼 보이지만, 구조 자체는 흔하다. 핵심은 팩토리의 __repr__ 이 자신을 참조하는 구조를 건드리게 되는 순간이다. from collections import defaultdict class Factory: def __init__(self): self.d = None de...
"보안 릴리즈인데 설치 파일이 없다": Python 3.10/3.11/3.12 소스-온리 보안 릴리즈를 실무에서 처리하는 법 보안 릴리즈 공지가 떴다. 팀 채널에 링크가 공유되고, 누군가가 한 줄 던진다. “이번에도 올려야죠?” 여기까지는 평소와 같다. 문제가 되는 건 다음 장면이다. 릴리즈 페이지로 들어가서 다운로드 섹션을 훑다가 멈춘다. 설치 파일이 없다. Windows 인스톨러도 없고, macOS 패키지도 없고, “그냥 받아서 설치”할 수 있는 게 없다. 그러면 대화가 이상해진다. “그럼 우리는 못 올리는 건가?” 아니다. 이건 ‘못 올리는’ 게 아니라, 업스트림이 “이번엔 이렇게 배포한다”라고 명확히 말해주는 구간이다. Python 3.10.20 / 3.11.15 / 3.12.13 릴리즈 페이지에는 보안 릴리즈이며(그리고 설치 파일이 없다는 점까지) 같이 적혀 있다(참고자료). 처음 겪으면 당황하지만, 실무에서는 이걸 우리 파이프라인이 어디에 의존하고 있는지 드러나는 신호 로 읽게 된다. 이 글은 CVE 번호를 나열하는 글이 아니다. 우리 팀이 오늘 해야 하는 건 “무서워하기”가 아니라 “어떻게 먹일지”를 결정하는 거다. 소스-온리 릴리즈를 운영에서 처리하는 흐름을, 실제로 막히는 지점에서부터 풀어보겠다. 여기서 말하는 ‘처리’는 거창한 프로젝트가 아니다. 오늘 안에 끝낼 수 있는 쪽으로 잘게 자르면 된다. 지금 서비스가 어떤 파이썬에서 돌고 있는지부터 확인하고, 그 파이썬이 어디서 들어오는지(베이스 이미지인지, 우리가 직접 빌드하는지)를 확인하고, 올렸다가 되돌리는 길이 열려 있는지까지 같이 본다. 이 흐름이 잡히면 “설치 파일이 없다”는 말은 장애가 아니라 공급 방식의 차이 로 내려온다. 본문에는 생 URL을 넣지 않고, 맨 아래 참고자료에만 링크를 둔다. 릴리즈 노트에서 멈추는 포인트: installers 없음은 ‘단계’의 문제다 Python Insider 공지와 Dis...