질문의 역사
Python 2.5 이전에는 try...finally와 try...except가 상호 배타적인 구문 블록으로 존재하여, 개발자가 오류 처리와 정리를 동시에 수행하기 위해 이를 어색하게 중첩해야 했습니다. PEP 341은 이러한 구조를 통합하여 finally가 try 블록이 종료되는 방식과 관계없이 실행된다는 현대적인 보장을 설정했습니다. 이러한 발전은 결정론적 소멸자가 없는 언어에서 신뢰할 수 있는 자원 관리 패턴을 구현하는 데 필수적이었습니다.
문제
개발자들은 종종 명시적인 return, break, 또는 continue 문이 현재 범위를 즉시 종료한다고 가정하는 경향이 있어, 후속되는 정리 코드를 건너뛰게 될 수 있습니다. finally 블록의 강제 실행이 없다면, try 블록 내에서 획득한 파일 핸들, 데이터베이스 연결 또는 잠금과 같은 자원이 조기 반환이 발생할 때마다 유출될 수 있습니다. 이는 프로덕션 시스템에서 자원 고갈, 교착 상태 또는 데이터 손상을 초래합니다.
해결 방법
Python의 컴파일러는 try...finally를 특정 바이트코드 명령어—SETUP_FINALLY, POP_BLOCK, END_FINALLY—로 변환하여 정리 핸들러를 해석기의 실행 프레임에 푸시합니다. return을 만나면 해석기는 반환 값을 값 스택에 푸시하고, finally 블록의 바이트코드를 실행한 후에야 대기 중인 반환을 처리합니다. 만약 finally 블록 자체가 return을 실행하거나 예외를 발생시킬 경우, 그 새로운 제어 흐름이 원래의 흐름을 초월하여 정리가 우선시됩니다.
def process_file(path): f = open(path, 'r') try: data = f.read() if not data: return None # Finally도 여전히 실행됩니다! return data.upper() finally: f.close() print("정리 완료")
문제 설명
재무 거래를 처리하는 마이크로서비스가 고부하 시 데이터베이스 연결 풀을 간헐적으로 고갈시키고 있었습니다. 조사를 통해 연결을 획득하고 캐시를 확인한 후 캐시 적중 시 조기 반환하는 헬퍼 함수에서 유출이 발생하는 것으로 밝혀졌습니다. 개발자는 conn.close() 호출을 함수의 끝에 두었고 항상 도달할 것이라고 가정했지만, 조기 반환이 이를 완전히 우회했습니다.
해결 방법 1: 수동 정리 중복
팀은 모든 return 문 이전에 conn.close() 호출을 복사하는 것을 고려했습니다. 그러나 이는 향후 수정으로 새로운 종료 지점이 추가될 수 있어 유지 관리가 불가능하다는 이유로 거부되었습니다. 또한 중복된 코드는 DRY 원칙을 위반했습니다. 이 접근 방식은 시각적인 혼란을 증가시키고 유지 관리 중 인간 오류의 위험을 높였습니다.
해결 방법 2: 컨텍스트 관리자
그들은 with get_connection() as conn:를 사용하기 위한 리팩토링을 평가했습니다. 이는 관용적이지만, 외부 연결 공장이 즉시 컨텍스트 관리자 프로토콜을 지원하도록 수정해야 했습니다. 공유 라이브러리 코드를 변경하는 위험이 즉각적인 배포가 필요한 핫픽스에 대한 이점을 초과했습니다.
해결 방법 3: Try-finally 래퍼
선택된 접근 방식은 연결 논리를 try...finally 블록으로 감싸는 것이었습니다. 이 최소한의 변화로 conn.close()가 반환되기 전에 반드시 실행되도록 보장하였고, 의존성 리팩토링 없이 즉각적인 안전성을 제공했습니다. 이는 미래의 유지 보수자에게 정리 보장을 명확히 전달했습니다.
결과
이 수정은 배포 몇 시간 내에 연결 유출을 제거했습니다. 이 패턴은 코드 베이스의 모든 자원 획득 함수에 대해 린팅 규칙을 통해 이후 의무화되었습니다. 이는 유사한 회귀를 방지하고 피크 부하에서 서비스를 안정화했습니다.
finally 블록이 함수의 반환 값을 수정하거나 억제할 수 있습니까?
예. finally 블록에 자신의 return 문이 포함되어 있다면, 이는 try 또는 except 블록에서 생성된 값을 덮어씁니다. 원래의 반환 값은 완전히 폐기됩니다. 또한, finally 블록이 예외를 발생시키면, 해당 예외는 이전 블록의 모든 예외 또는 반환 값을 대체하여 원래 결과를 억제합니다.
try 블록에서 발생한 예외가 finally 블록에서도 예외가 발생하면 어떻게 되나요?
원래 예외는 마스킹으로 인해 사라집니다. Python은 finally 블록에서 예외를 발생시키고, 초기 예외의 추적 정보는 명시적으로 캡처되지 않는 한 폐기됩니다. 이를 방지하기 위해 finally 블록에서는 예외가 발생할 수 있는 작업을 피하거나, finally 내에 중첩된 try...except를 사용하여 정리 오류를 우아하게 처리하면서 원래 예외 컨텍스트를 보존해야 합니다.
finally 블록이 실행되지 않을 것이 보장되는 상황이 있습니까?
Python의 언어 의미론은 정상적인 제어 흐름에 대해 finally가 실행될 것을 보장하지만, 특정 재앙적인 사건은 이를 우회합니다. 운영 체제가 SIGKILL과 같은 잡을 수 없는 신호를 보내거나, os._exit()가 호출되거나, Python 프로세스가 세그멘테이션 결함으로 인해 충돌할 경우, 해석기는 즉시 종료되며 대기 중인 finally 블록을 실행하지 않습니다. 또한, try 블록 내의 무한 루프나 교착 상태는 finally 절에 도달하는 것을 완전히 방지합니다.