1. 문제 정의
검증 환경
본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.
- 본문에 명시된 오류 메시지·프레임워크 버전을 기준으로 원인을 추적했다.
- 문서 정리일: 2026-09-01
- OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.
asyncio.print_call_graph()는 실행 중인 태스크의 호출 그래프를 텍스트로 출력하는 디버깅용 헬퍼 함수입니다. 그런데 이미 완료된 태스크에 대해 호출하면 그래프를 출력하는 대신 AttributeError로 크래시하는 버그가 있습니다.
에러 원문:
asyncio.print_call_graph()raisesAttributeErroron a finished task
1AttributeError: 'NoneType' object has no attribute 'f_generator'
이슈에 제시된 재현 코드는 다음과 같습니다.
| |
핵심은 태스크 t를 await로 완료시킨 뒤 print_call_graph(t)를 호출하는 것입니다.
- 실제 출력:
1AttributeError: 'NoneType' object has no attribute 'f_generator' - 기대 출력:
1* Task(name='Task-2', id=0x103fc49b0)
2. 원인 탐구 (표면 증상)
에러 메시지의 f_generator는 프레임(frame)에서 코루틴으로 거슬러 올라가는 속성입니다. AttributeError: 'NoneType' object has no attribute 'f_generator'라는 점에서, print_call_graph() 내부가 코루틴의 프레임 객체에 접근하다가 그 대상이 None임을 만났음을 짐작할 수 있습니다.
즉 표면적으로는 “그래프 그리기가 실패했다"이지만, 이면에는 코루틴 프레임이 None이 될 수 있다는 사실을 처리하지 못하는 검사 누락이 있습니다.
증상 fingerprint
| 항목 | 내용 |
|---|---|
| 에러 메시지 | AttributeError: 'NoneType' object has no attribute 'f_generator' |
| 발생 단계 | 태스크가 완료된 이후 print_call_graph() 호출 시점 |
| 관련 도구 | Python 표준 라이브러리 asyncio, CPython 코루틴 프레임 구조 |
| 흔한 오해 | “태스크 객체가 잘못됐다”, “인자를 잘못 넘겼다” — 둘 다 아님 |
3. 근본 원인 분석
print_call_graph()는 호출 그래프를 그릴 때 코루틴 체인을 따라 프레임을 수집합니다. 이슈에 제안된 수정 코드의 구조에서 내부 로직을 유추하면 다음과 같습니다.
| |
여기서 coro.cr_frame이 None일 수 있는데, 이 사실을 검사하지 않고 곧바로 FrameCallGraphEntry(coro.cr_frame)에 넘기기 때문에 내부에서 프레임의 f_generator에 접근하려다 NoneType AttributeError가 발생합니다.
왜 완료된 코루틴의 cr_frame은 None이 되는가
CPython에서 네이티브 코루틴의 cr_frame은 코루틴이 아직 완료되지 않았을 때만 활성 프레임을 가리킵니다. 코루틴이 끝나면 실행 프레임이 해제되면서 cr_frame은 None이 됩니다.
재현 코드에서 await t로 태스크 t가 실행을 마쳤기 때문에, 그 시점의 t가 실행하던 코루틴의 cr_frame은 이미 None입니다. print_call_graph()가 이 None을 감지하지 못하고 프레임을 만들려다 크래시한 것입니다.
원인 후보 매트릭스
| 원인 후보 | 확인 방법 | 맞는 경우의 증상 | 해결 방향 |
|---|---|---|---|
| 코루틴 체인(cr_await)이 None | 루프에서 coro 값 추적 | while 문에서 조용히 종료됨, 크래시 아님 | 해당 없음 |
| 프레임(cr_frame)이 None (본 사례) | 완료된 태스크의 cr_frame 확인 | None 출력 + f_generator AttributeError | cr_frame None 검사 후 프레임 추가 건너뛰기 |
| 태스크 객체가 잘못됨 | isinstance(t, asyncio.Task) | 타입 오류 | 올바른 태스크 전달 |
본 사례는 가운데 행에 해당합니다. cr_frame이 None인지 검사하는 로직이 빠져 있는 것이 근본 원인입니다.
4. 코드 해결책
이슈 본문에 제안된 수정은 네이티브 코루틴 분기에서 cr_frame이 None이 아닐 때만 프레임을 추가하고, 그 다음 코루틴(cr_await)으로 이동하도록 하는 것입니다.
| |
변경은 두 부분입니다.
- 기존:
st.append(FrameCallGraphEntry(coro.cr_frame))— None이든 아니든 무조건 추가 시도 - 수정:
if coro.cr_frame is not None: st.append(...)— None이 아닐 때만 추가
이렇게 하면 이미 완료되어 cr_frame이 None이 된 코루틴에서도 프레임을 건너뛰고 cr_await를 따라 계속 탐색할 수 있어 크래시 없이 그래프를 그릴 수 있습니다.
검증 명령
| |
실행 결과로 NoneType AttributeError가 더는 발생하지 않고 태스크 그래프가 출력되는지 확인하면 됩니다.
5. 향후 예방 조치
실전 적용 체크
| 환경 | 확인할 점 |
|---|---|
| 로컬 개발 | 완료된 태스크를 디버깅 헬퍼에 넘길 때 cr_frame/ag_frame의 None 처리를 확인 |
| CI | asyncio 디버깅 API를 쓰는 테스트에 완료된 태스크 경로를 포함해 회귀 테스트 |
| 프로덕션 | 코루틴 프레임 접근은 진단 코드에 한정하고 코루틴을 불필요하게 오래 보관하지 않기 |
잘못된 해결책 (권장하지 않음)
- 완료된 코루틴을 참조로 오래 살려두기: cr_frame을 유지하려고 코루틴/태스크를 장기 보관하는 것은 메모리와 동작을 왜곡할 수 있습니다. 이 문제는 호출 그래프 도구의 None 검사 누락이므로 도구 쪽에서 None을 처리해야 합니다.
- 예외를 try/except로 무시하며 넘기기: 그래프가 그려지지 않는 채 예외만 삼키면 진짜 버그를 숨기고 도구가 목적한 호출 그래프 출력을 달성하지 못합니다.
버전별 주의
이 이슈는 CPython main 브랜치에서 asyncio.print_call_graph()를 사용할 때 확인되었습니다. print_call_graph()는 모든 지원 버전에 항상 존재하지 않을 수 있으므로, 사용 전에 해당 함수가 지원되는지 확인하는 것이 안전합니다. (이는 문제 원인이 아니라 일반적인 점검 기준입니다.)
DevTrace verdict
핵심은 완료된 코루틴의 cr_frame이 None이 될 수 있는데 asyncio.print_call_graph()가 이를 검사하지 않고 프레임을 만들려다 f_generator 접근에서 AttributeError를 던진다는 None 체크 누락입니다.
DevTrace 결론
핵심은 코루틴 자체가 사라진 것이 아니라, 완료된 코루틴의 cr_frame이 None이 되는데 호출 그래프 탐색 루프가 그 사실을 처리하지 못하는 검사 누락이다.
원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.