모든 이야기

예외 처리는 실패한 함수에서 어디로 제어를 옮길까

함수가 파일을 읽거나 숫자를 변환할 때 정상적인 결과를 만들지 못할 수 있다. 예외 처리는 이런 상황에서 일반적인 실행 흐름을 중단하고, 해당 조건을 처리하도록 등록된 코드로 제어를 옮기는 방식이다. 예외는 보통 종류와 메시지, 관련 정보를 담는 값으로 표현된다. 단순히 오류 문구를 출력하는 것보다 호출 관계 전체에서 실패를 전달할 수 있다는 점이 중요하다.

C에서 발생한 예외가 B를 지나 A의 처리기로 전달되며 종료되는 호출을 정리하는 흐름

예외 전달은 호출 흐름을 따라 처리기를 찾고, 종료되는 범위의 정리를 수행한다.

호출한 함수까지 실패가 올라간다

A가 B를, B가 C를 호출했는데 C에서 예외가 발생했다고 하자. C 안에 맞는 처리기가 없으면 호출한 B 쪽으로 전달되고, 거기에도 없다면 A 쪽을 찾는다. 이때 종료되는 호출 프레임을 정리하는 과정을 스택 풀기라고 한다. 처리기의 선택은 실제 호출 흐름과 예외 종류에 따른 규칙을 따른다. 실패 직전의 다음 줄을 그냥 계속 실행하는 것과는 다르다.

Python의 try 블록에서 int(text)를 실행하고 except ValueError로 변환 실패를 다룰 수 있다. 이 경우 실패한 변환의 반환값을 사용하는 나머지 try 문장은 건너뛰고 처리기가 실행된다. Python 예외 교육 자료는 일치하는 처리기가 없을 때 예외가 호출 바깥으로 전달되는 동작과 정리 구문을 설명한다. 복구하려면 재입력이나 재시도를 명시적으로 수행해야 한다.

처리와 정리는 역할이 다르다

finally는 성공과 실패 여부에 관계없이 필요한 정리를 수행하도록 사용한다. 파일을 열었다면 처리 중 예외가 생겨도 닫아야 하고, 잠금을 잡았다면 적절한 경로에서 풀어야 한다. Python의 with나 다른 언어의 범위 기반 자원 관리도 이 목적에 유용하다. 정리 코드는 오류를 해결한 것으로 간주하는 코드와 다르며, 예외를 계속 전달하면서 자원만 반환할 수 있다.

정리 구문에서 새로운 예외를 던지거나 값을 반환하면 원래 실패의 전달에 영향을 줄 수 있다. 특히 Python에서 finally 안의 return은 원래 예외를 숨길 수 있으므로 주의해야 한다. 예외가 발생했을 때 중간까지 갱신된 데이터가 어떤 상태인지도 살펴야 한다. 메모리나 파일을 닫았다고 작업의 논리적 일관성까지 자동으로 복구되지는 않는다.

어디에서 잡아야 의미 있는 복구가 될까

입력 형식이 틀린 경우에는 사용자에게 다시 입력받을 수 있다. 하지만 잘못된 내부 상태나 프로그래밍 오류까지 모두 잡아 빈 결과로 바꾸면 문제가 조용히 확대될 수 있다. 가장 가까운 위치에서 무조건 처리하기보다, 그 실패의 의미를 알고 복구 정책을 결정할 수 있는 경계에서 처리하는 편이 낫다. 로그에는 원래 원인을 추적할 정보를 남기는 것이 중요하다.

재시도도 예외를 잡는 것만으로 안전하지 않다. 서버가 실제로 주문을 저장한 뒤 응답 전 연결이 끊겼다면 같은 요청을 다시 보내 중복 주문을 만들 수 있다. 작업이 반복돼도 같은 결과를 유지하는지, 요청 식별자가 있는지, 부분 수행을 되돌릴 수 있는지 확인해야 한다. 실패를 전달하는 메커니즘과 실패 후 정책은 서로 다른 설계 문제다.

일부 언어와 API는 예외 대신 오류를 포함한 반환값을 사용한다. 이 방식은 호출자가 결과를 명시적으로 분기하게 하고, 예외 방식은 여러 호출 단계를 건너뛰어 전달하기 쉽다. 어떤 방식을 고르든 오류의 종류, 책임 위치, 정리 절차가 분명해야 한다. 예외 처리를 잘 설계한다는 것은 실패를 숨기는 것이 아니라 정상 흐름이 끊긴 뒤 프로그램이 유지해야 할 약속을 정하는 일이다.

참고 자료

예외 처리는 실패한 함수에서 어디로 제어를 옮길까 · ariser