[Draft] PEP 785 - New methods for easier handling ofExceptionGroups
원문 링크: PEP 785 - New methods for easier handling ofExceptionGroups
상태: Draft 유형: Standards Track 작성일: 08-Apr-2025
PEP 785는 ExceptionGroup
을 더 쉽게 다룰 수 있는 새로운 메서드들을 제안합니다. 이 PEP는 Python 3.14 버전을 대상으로 하며, BaseExceptionGroup.leaf_exceptions()
와 BaseException.preserve_context()
라는 두 가지 새로운 메서드를 추가하여 예외 처리 로직을 간결하게 표현하고 디버깅 경험을 개선하는 것을 목표로 합니다.
PEP 785: ExceptionGroup 처리를 위한 새로운 메서드
초록 (Abstract)
PEP 654에서 도입된 ExceptionGroup
이 Python 커뮤니티 전반에 걸쳐 널리 사용됨에 따라, 일반적이지만 다루기 어려운 패턴들이 나타나고 있습니다. 이에 따라 예외 객체에 두 가지 새로운 메서드를 추가할 것을 제안합니다.
BaseExceptionGroup.leaf_exceptions()
: 중간 그룹으로부터 합성된 트레이스백(traceback)을 포함하여 ‘리프(leaf)’ 예외들을 리스트로 반환합니다.BaseException.preserve_context()
:self
의self.__context__
속성을 저장하고 복원하는 컨텍스트 관리자(context manager)입니다. 이를 통해 다른 핸들러 내에서 예외를 다시 발생(re-raising)시킬 때 기존 컨텍스트가 덮어씌워지는 것을 방지합니다.
이 메서드들은 중간 복잡도의 여러 경우에서 에러 처리 로직을 더 간결하게 표현할 수 있도록 할 것으로 예상됩니다. 이 메서드들이 없으면 예외 그룹 핸들러는 중간 트레이스백을 계속해서 버리고 __context__
예외를 잘못 처리하여, 비동기(async) 코드 디버깅에 어려움을 초래할 것입니다.
동기 (Motivation)
ExceptionGroup
이 널리 사용되면서, 라이브러리 작성자와 최종 사용자는 미들웨어, 에러 로깅, 웹 프레임워크의 응답 핸들러를 구현할 때 개별 리프 예외(individual leaf exceptions)를 처리하거나 응답하는 코드를 자주 작성합니다.
GitHub 검색 결과, leaf_exceptions()
와 유사한 기능을 다양한 이름으로 구현한 사례가 60건 중 4건 발견되었으며, 이들 중 트레이스백을 올바르게 처리하는 경우는 없었습니다. 또한, leaf_exceptions()
를 사용할 수 있는 사례는 13건 발견되었습니다. 따라서, 올바른 트레이스백 보존 기능을 갖춘 BaseException
타입의 메서드를 제공함으로써 전체 생태계의 에러 처리 및 디버깅 경험이 향상될 것으로 예상됩니다.
ExceptionGroup
의 등장은 이전에 잡힌 예외를 다시 발생시키는 경우를 훨씬 더 흔하게 만들었습니다. 예를 들어, 웹 서버 미들웨어는 HTTPException
이 그룹의 유일한 리프인 경우 이를 언랩(unwrap)할 수 있습니다. 그러나, raise first
는 first.__context__ = group
이라는 부작용을 발생시켜 원래 에러의 컨텍스트를 버리게 됩니다. 이는 에러가 발생한 이유를 이해하는 데 중요한 정보를 포함할 수 있는 원래 컨텍스트를 손상시키며, 프로덕션 환경에서 트레이스백이 수백 줄에서 수만, 수십만 줄로 부풀어 오르게 하여 에러 이해를 훨씬 더 어렵게 만듭니다.
새로운 BaseException.preserve_context()
메서드는 이러한 경우에 발견 가능하고(discoverable), 읽기 쉬우며, 사용하기 쉬운 해결책이 될 것입니다.
명세 (Specification)
BaseExceptionGroup.leaf_exceptions()
메서드
BaseExceptionGroup
에 다음과 같은 시그니처를 가진 leaf_exceptions()
메서드가 추가될 것입니다.
def leaf_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]:
"""
그룹 내의 모든 '리프' 예외의 평면화된(flat) 리스트를 반환합니다.
fix_tracebacks가 True인 경우, 각 리프의 트레이스백은 중간 그룹에 첨부된
프레임들이 디버깅 시에도 보이도록 합성된(composite) 트레이스백으로 대체됩니다.
이러한 변경을 비활성화하려면 fix_tracebacks=False를 전달하세요.
예를 들어, 그룹을 변경하지 않고 다시 발생시킬(raise) 경우에 사용합니다.
"""
BaseException.preserve_context()
메서드
BaseException
에 다음과 같은 시그니처를 가진 preserve_context()
메서드가 추가될 것입니다.
def preserve_context(self) -> contextlib.AbstractContextManager[Self]:
"""
예외의 __context__ 속성을 보존하는 컨텍스트 관리자입니다.
컨텍스트에 진입할 때 __context__의 현재 값이 저장됩니다.
종료할 때 저장된 값이 복원되어, except 블록 내에서 예외를 발생시켜도
해당 예외의 컨텍스트 체인을 변경하지 않도록 합니다.
"""
사용 예시:
# 비동기 웹 프레임워크에서, 사용자 코드는 특정 HTTP 에러 코드를 클라이언트에
# 반환하기 위해 HTTPException을 발생시킬 수 있습니다.
# 그러나, TaskGroup 내에서 발생할 수도 있고 아닐 수도 있으므로 `except*`를
# 사용해야 합니다. 만약 여러 개의 HTTPException이 있다면 버그로 처리합니다.
try:
user_code_here()
except* HTTPException as group:
first, *rest = group.leaf_exceptions()
if rest:
raise # 내부 서버 에러 미들웨어에 의해 처리됨
...
# 로깅, 캐시 업데이트 등
with first.preserve_context():
raise first
.preserve_context()
가 없으면, 위 코드는 예외의 기존 __context__
를 버리거나, except*
블록 밖에서 예외를 발생시키도록 코드를 복잡하게 만들거나, preserve_context()
의 의미를 인라인으로 구현해야 했을 것입니다.
하위 호환성 (Backwards Compatibility)
내장 클래스, 특히 BaseException
과 같이 널리 사용되는 클래스에 새로운 메서드를 추가하는 것은 상당한 영향을 미 미칠 수 있습니다. 그러나 GitHub 검색 결과 이 메서드 이름들과의 충돌은 발견되지 않았습니다. 만약 사설 코드에 동일한 이름의 사용자 정의 메서드가 존재한다면, PEP에서 제안하는 메서드를 덮어쓰게 되지만 런타임 동작에는 영향을 미치지 않습니다.
교육 방법 (How to Teach This)
ExceptionGroup
작업은 초급 프로그래머에게는 발생하기 어려운 중급에서 고급 주제입니다. 따라서 이 주제는 문서를 통해, 그리고 정적 분석 도구의 적시 피드백을 통해 교육할 것을 제안합니다. 중급 과정에서는 .leaf_exceptions()
를 .split()
및 .subgroup()
메서드와 함께 가르치고, .preserve_context()
는 특정 문제점을 해결하기 위한 고급 옵션으로 언급하는 것을 권장합니다.
API 참조 문서와 기존 ExceptionGroup
튜토리얼은 새로운 메서드를 시연하고 설명하도록 업데이트되어야 합니다. 튜토리얼에는 .leaf_exceptions()
와 .preserve_context()
가 에러 처리 로직을 단순화하는 일반적인 패턴의 예시를 포함해야 합니다. ExceptionGroup
을 자주 사용하는 다운스트림 라이브러리도 유사한 문서를 포함할 수 있습니다.
또한, flake8-async
에 포함될 린트(lint) 규칙을 설계하여 group.exceptions
를 반복하거나 리프 예외를 다시 발생시킬 때 .leaf_exceptions()
사용을 제안하고, except*
블록 내에서 리프 예외를 다시 발생시킬 때 기존 컨텍스트가 덮어씌워지는 경우 .preserve_context()
사용을 제안할 것입니다.
채택되지 않은 아이디어 (Rejected Ideas)
- 메서드 대신 유틸리티 함수 추가: 메서드보다 유틸리티 함수를 추가하는 아이디어가 있었으나, 헬퍼 함수가 어디에 위치해야 할지 명확하지 않고, 인수가
BaseException
인스턴스여야 하며, 메서드가 더 편리하고 발견하기 쉽다는 이유로 거부되었습니다. BaseException.as_group()
(또는 그룹 메서드) 추가: 단일 예외와 그룹 내 동일 유형 예외를 모두 처리하는 중복된 로직이 많다는 점을 고려하여as_group()
메서드가 제안되었지만, 기존 코드 리팩토링 시 개선 효과가 미미하여 거부되었습니다. 대신 중복 제거된 에러 처리를 위한 “그룹으로 변환” 레시피를 문서화할 것을 권장합니다.- 컨텍스트 관리자 대신
e.raise_with_preserved_context()
추가: 컨텍스트 관리자 형태가raise ... from ...
을 허용하고, 덜 마법적이며 부적절한 경우에 사용될 가능성이 적다는 이유로 선호되었습니다. - 추가 속성 보존:
__cause__
및__suppress_context__
속성은 예외 재발생 시 변경되지 않으므로 보존하지 않기로 결정했습니다.__traceback__
속성 보존도 고려되었으나, 추가raise ...
문이 에러 이해에 중요한 단서가 될 수 있으므로 거부되었습니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인 또는 CC0-1.0-Universal 라이선스 중 더 관대한 조건에 따라 배포됩니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments