← 문제로

25. try/finally 스택 되감기와 예외 필터(when)의 실행 시점 차이

난이도 최상
내 리뷰 · C#
해설 · C#

해설 — try/finally 스택 되감기와 예외 필터(when)의 실행 시점 차이

난이도: 최상

요약

C#의 예외 필터(catch (Ex e) when (condition))는 스택 되감기(stack unwinding)가 시작되기 이전, 예외가 발생한 바로 그 콜스택이 아직 살아있는 상태에서 평가된다. 필터가 false를 반환하면 스택은 되감기지 않고 예외는 그대로 상위로 전파(다른 필터/핸들러 탐색)되며, 필터가 true를 반환해야 비로소 되감기가 시작되고 그제서야 그 사이의 finally 블록들이 실행된 뒤 catch 본문이 실행된다. 따라서 problem.cs를 실행하면 [filter][finally]보다 먼저 출력된다.

핵심 개념

  • 스택 되감기(Stack Unwinding): 예외를 처리할 핸들러가 확정된 뒤, 예외 발생 지점부터 그 핸들러까지의 콜스택에 있는 모든 finally 블록을 실행하며 스택 프레임을 정리해 나가는 과정.
  • 예외 필터(Exception Filter): when 절의 조건식. CLR이 예외 처리 핸들러를 "탐색"하는 1차 패스(first-pass) 동안 평가되며, 이 시점에는 아직 스택이 되감기지 않은 상태다.
  • 2-패스 예외 처리 모델: CLR의 예외 처리는 (1) 핸들러를 찾는 탐색 패스 — 이 안에서 필터가 평가됨 — 와 (2) 핸들러가 확정된 후 스택을 되감고 실제로 실행하는 패스, 두 단계로 나뉜다.

단계별 내부 동작

  1. Main()ProcessPacket(1) → 내부 tryHandlePacket(1) 호출. [handle] packet=1 처리 시작 출력.
  2. HandlePacket 안에서 throw new InvalidOperationException(...) 실행. 이 시점부터 CLR의 예외 탐색(1차 패스)이 시작된다.
  3. CLR은 콜스택을 위로 올라가며 이 예외를 처리할 수 있는 catch를 찾는다. 먼저 안쪽 try에는 catch가 없고 finally만 있으므로 그냥 지나친다(아직 실행하지 않음 — finally 실행은 되감기 패스에서만 일어난다).
  4. 바깥쪽 trycatch (Exception e) when (LogAndCheck(e))에 도달하면, 되감기 전에 필터 조건식을 평가한다. 이때 LogAndCheck(e)가 호출되어 [filter] 필터 평가 중... (e=InvalidOperationException)이 출력된다. 이 시점의 호출 스택에는 아직 HandlePacket, 예외가 던져진 지점까지의 프레임이 그대로 남아 있다(스택이 아직 되감기지 않았기 때문).
  5. LogAndChecktrue를 반환하므로 이 catch가 핸들러로 확정된다. 이제 CLR은 2차 패스(되감기)를 시작해, 예외 발생 지점부터 이 catch까지 사이에 있는 finally 블록들을 실행한다 — 여기서 내부 tryfinally가 실행되어 [finally] 패킷 처리 종료 (내부 리소스 정리)가 출력된다.
  6. 되감기가 끝난 뒤 비로소 catch 본문이 실행되어 [catch] 예외 처리: 잘못된 패킷 형식이 출력된다.
  7. 따라서 정확한 출력 순서는 다음과 같다:
    [handle] packet=1 처리 시작
    [filter] 필터 평가 중... (e=InvalidOperationException)
    [finally] 패킷 처리 종료 (내부 리소스 정리)
    [catch] 예외 처리: 잘못된 패킷 형식
    

흔한 오해·함정

  • "finally는 예외가 발생하면 무조건 catch보다 먼저, 그리고 필터보다도 먼저 실행될 것"이라고 오해하기 쉽다. 실제로는 필터 평가가 finally보다 먼저 일어난다 — 필터는 "이 핸들러가 이 예외를 잡을지 말지"를 결정하는 탐색 단계의 일부이고, finally는 핸들러가 확정된 뒤의 정리 단계에 속하기 때문이다.
  • 필터 함수(LogAndCheck) 안에서 Environment.StackTrace를 찍거나 디버거로 콜스택을 보면, 일반 catch 블록 안에서 보는 것보다 훨씬 깊은(예외가 실제로 던져진 지점에 가까운) 스택을 보게 된다 — 이는 필터가 되감기 이전에 실행되기 때문이며, 크래시 덤프 분석이나 원인 추적에 유용하게 활용되기도 한다(예: when (LogFullStackAndReturnFalse(e)) 패턴으로 원본 스택을 로깅한 뒤 항상 false를 반환해 다른 핸들러로 넘기는 기법).
  • 반대로, 필터 안에 부작용(side effect)이 있는 로직 — 예: 카운터 증가, 외부 리소스 접근 — 을 넣으면, 아직 finally로 정리되지 않은 리소스 상태에서 그 로직이 실행되는 셈이 되어 예상치 못한 순서 의존 버그를 만들 수 있다. 특히 필터가 여러 번 평가될 수 있는 상황(여러 catch 절 후보가 있을 때 순서대로 필터가 시도됨)에서는 필터 부작용이 여러 번 실행될 수도 있다는 점도 주의해야 한다.

면접 포인트

  • 예외 필터(when)의 평가 시점이 스택 되감기 이전이라는 것과, 그로 인한 정확한 실행 순서(필터 → finally → catch)를 근거를 들어 설명할 수 있는가.
  • 이 특성이 디버깅/로깅(원본 스택 보존)에 어떻게 실무적으로 활용될 수 있는지 예를 들 수 있는가.
  • 필터 안에 부작용이 있는 코드를 넣었을 때 생길 수 있는 문제(정리되지 않은 리소스 상태에서 실행됨, 여러 번 평가될 수 있음)를 지적할 수 있는가.