역사: PEP 343은 Python 2.5에서 with 문을 도입하여 이전에 장황한 수동 try-finally 블록이 필요했던 자원 관리 패턴을 표준화했습니다. 이 프로토콜은 객체가 __enter__ 및 __exit__ 메서드를 구현할 것을 요구하며, 중요한 혁신은 __exit__가 예외를 검사하고 선택적으로 억제할 수 있는 반환 값을 가진다는 것입니다. 이 디자인은 인프라가 예상되는 실패를 처리할 수 있는 우아한 저하 패턴을 가능하게 하며, 이를 비즈니스 로직에 전파하지 않습니다.
문제: with 블록 내에서 예외가 발생하면, Python은 활성화된 예외의 세부 정보를 가진 __exit__(exc_type, exc_val, exc_tb)를 호출합니다. 이 메서드가 참(t)을 반환하는 경우(Python은 이를 논리적 맥락에서 True로 평가함) Python은 예외가 처리되었다고 판단하고 전파를 완전히 억제합니다. 반면, False, None 또는 어떤 거짓 값이 반환되면, 세척이 성공했는지 여부에 관계없이 예외가 정상적으로 전파됩니다.
해결책: 예외가 의도적으로 억제되어야 할 때만 True를 반환하도록 __exit__를 구현합니다. 예를 들어, 예상되는 유효성 검사 오류나 일시적인 네트워크 실패에 대해 True를 반환합니다. 세척이 완료되었지만 오류를 전파해야 할 경우 False를 명시적으로 반환하거나 메서드의 끝에서 빠져나와 암묵적으로 None을 반환합니다. 이 메서드는 활성 예외를 설명하는 세 개의 인수를 받거나 정상적으로 종료되면 (None, None, None)을 받습니다.
class SuppressKeyError: def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is KeyError: print(f"Swallowed: {exc_val}") return True # 억제 return False # 다른 것들은 전파 # 사용법 with SuppressKeyError(): raise KeyError("ignored") # 침묵 with SuppressKeyError(): raise ValueError("propagated") # 전파됨
시나리오: 개발 팀이 작업자 노드가 중요한 섹션을 실행하기 전에 Redis를 통해 독점 잠금을 획득하는 분산 작업 프로세서를 구축합니다. 네트워크 지연으로 인해 LockTimeout 예외가 발생할 경우, 시스템은 작업자 프로세스를 충돌시키는 대신 투명하게 재시도해야 합니다. 그러나 MemoryError와 같은 치명적인 오류나 프로그래밍 실수는 즉시 전파되어 경고를 트리거하고 무한 재시도 루프를 방지해야 합니다.
문제: 초기 구현은 비즈니스 로직 전반에 걸쳐 try-except 블록이 분산되어 있어 유지 관리 소란을 유발하고 실제 도메인 코드를 모호하게 만들었습니다. 인프라 문제를 도메인 코드에 오염시키지 않으면서 이 선택적 억제 메커니즘을 중앙 집중화하는 것이 도전 과제입니다.
해결책 1: 각 작업 실행을 호출 지점에서 명시적인 중첩 try-except 블록으로 감쌉니다. 장점: 제어 흐름이 비즈니스 로직 독자에게 즉시 가시화되어 새로운 팀원이 디버깅을 간단하게 할 수 있습니다. 단점: 이 접근 방식은 재시도 로직을 모든 곳에서 반복하여 DRY 원칙을 위반하며, 비즈니스 코드를 인프라 세부사항에 강하게 연결하고, 각 호출 지점에서 잠금 실패를 시뮬레이션해야 하므로 단위 테스트를 어렵게 만듭니다.
해결책 2: DumbSuppressor 컨텍스트 관리자를 만들어 __exit__에서 무조건적으로 True를 반환합니다. 장점: 구현은 단 두 줄의 코드만 필요하며 비즈니스 로직에서 예외 처리 보일러플레이트를 완전히 제거합니다. 단점: 이 방법은 모든 예외, 특히 치명적인 시스템 오류와 프로그래밍 버그를 위험하게 억제하여 침묵의 실패와 정의되지 않은 애플리케이션 상태가 되어 운영 환경에서 디버깅이 불가능하게 만듭니다.
해결책 3: SmartRetryContext를 구현하여 exc_type을 일시적인 예외의 구성 가능한 화이트리스트와 비교합니다. 장점: 이는 재시도 로직을 선언적으로 중앙 집중화하고 어떤 오류가 재시도를 촉발하고 즉시 전파될지를 정밀하게 제어하며 비즈니스 로직과 인프라 문제 사이의 깔끔한 분리를 유지합니다. 단점: 화이트리스트는 예기치 않은 오류를 우연히 억제하지 않도록 신중하게 유지 관리해야 합니다. 이러한 오류는 일시적인 인프라 문제를 나타내는 것이 아니라 실제 버그를 나타낼 수 있습니다.
선택한 접근 방식: 팀은 안전성과 기능의 균형을 맞추기 위해 해결책 3을 선택했습니다. __exit__ 메서드는 issubclass(exc_type, RetriableException)를 검사하고 네트워크 시간 초과와 같은 일시적인 실패에 대해 True를 반환하며, 프로그래밍 오류는 즉시 디버깅을 위해 드러나도록 합니다.
결과: 이 시스템은 자동으로 재시도하여 Redis 지연 급증을 우아하게 처리하면서도 버그가 발생할 경우 적절히 중단합니다. 모니터링 대시보드는 일시적인 실패로 인한 경고 소음의 40% 감소를 보여주었고, 개발자는 잠금 획득 세부정보에 대한 걱정 없이 작업 로직을 작성할 수 있었습니다.
질문: __exit__ 메서드가 None을 반환할 때와 False를 반환할 때의 행동을 구별하는 것은 무엇이며, 두 경우 모두 전파가 발생하는 이유는 무엇인가? Python에서는 반환 값이 불리할 경우 두 경우 모두 전파됩니다.
답변: 많은 후보자들은 None을 반환하면 "의견 없음"을 의미하고 False가 전파를 적극적으로 요청한다고 잘못 믿습니다. Python에서는 두 값이 모두 불리하며 프로토콜은 명시적으로 if not exit_return_value: propagate_exception()을 확인합니다. 따라서 None과 False는 동일하게 작동하며 예외가 두 경우 모두 전파됩니다. 구별은 코드 가독성에만 중요합니다; False는 의도적인 전파를 나타내고 None은 우연한 누락을 나타냅니다.
질문: Python의 __exit__ 메서드가 의도적으로 True를 반환하여 예외를 억제하지만, 정리 로직 중에 새로운 예외를 발생시키면 어떤 예외가 외부 범위로 전파되는가?
답변: __exit__에서 발생한 새로운 예외는 원래 예외를 완전히 대체합니다. Python은 먼저 __exit__의 반환 값을 평가합니다; 만약 이것이 참이면 원래 예외를 억제할 준비를 합니다. 그러나 __exit__ 자체가 반환되기 전에 예외를 발생시키면 그 새로운 예외가 대신 전파되고 원래의 예외는 raise NewException from original을 사용해 명시적으로 체인하지 않으면 손실됩니다. 이는 finally 블록과 다르며, finally 블록의 예외는 현재 예외를 대체하지만 활성 예외와 연결될 수 있습니다.
질문: 어떤 조건에서 Python은 __enter__에 진입한 후 exit이 호출되지 않도록 보장하며, 이것이 finally 블록 보장과 어떻게 다른가?
답변: 만약 __enter__가 예외를 발생시키면 Python은 __exit__를 절대 호출하지 않습니다. 왜냐하면 컨텍스트가 성공적으로 설정되지 않았기 때문입니다. 이는 try-finally 의미와 극명히 대비됩니다; try 구문이 진입 직후 예외를 발생시키더라도 finally 블록은 실행됩니다. 이 구별은 자원 관리에 있어 매우 중요합니다: 실패 전에 __enter__에서 부분적으로 할당된 자원은 finally로 정리해야 하며 __exit__는 그들을 정리하기 위해 실행되지 않을 것입니다.