타이머 루틴의 예외는 왜 전파가 되지 않나요?

@codemaru · December 05, 2008 · 8 min read

어젠가 뉴스그룹에서 재미난 질문을 보았습니다.
질문의 내용은 다음과 같습니다.

이번에 아주 재미난 것을 발견했다. 다른 여러 사람들도 이 문제를 겪은 것 같은데 해결 방법이나 문서에 커멘트가 하나도 없다. 내용은 다음과 같다.

타이머 콜백 루틴에서 강제로 예외를 발생 시켰다. 그랬는데 예외는 전파되지 않고, 해당 콜백 함수는 아주 조용히 실패했다. 디버거로 조사해보니 주기적으로 콜백 함수는 호출이 되었고, ACCESS VIOLATION(0xC0000005)이 발생하는 것을 확인했다. 분명히 win32 내부의 어떤 코드에서 예외를 잡아먹고 있음이 확실했다. 나는 이 문제 때문에 타이머 콜백 루틴의 버그를 잡은데 아주 고생했다.

예외에 대해서 처음 접하신 분들이 생각할 수 있을 법한 질문입니다. 특히나 콜백 루틴의 버그가 위와 같은 상황으로 조용히 실패한 경우에는 더욱 찾기 힘들어지죠. 그럼 더 열받게되고 왜 이런 디자인을 했는지에 대한 불평을 늘어놓게 됩니다. 하지만 명백하게 이는 의도된 디자인이며 다른 모든 모듈도 win32와 같은 구조를 취하는게 좋습니다.

우선 타이머의 콜백 루틴에서 발생한 예외가 전파되지 않은 이유를 살펴봅시다. 이는 윈도우 내부 코드에서 콜백을 호출하는 부분을 SEH로 보호하고 있기 때문입니다. 보통 아래와 같은 루틴을 통해서 콜백을 호출합니다.

... 콜백 호출 전 작업 ...  
  
\_\_try { your\_timer\_callback(); }  
\_\_except(EXCEPTION\_EXECUTE\_HANDLER) {}  
  
... 콜백 호출 후 작업 ...

이제 왜 예외가 전파되지 않았는지는 명백해지죠. SEH 때문에 잡아먹힌 거죠. 그렇다면 왜 콜백을 SEH로 보호해서 호출했을까요. 이유는 간단합니다. SEH로 보호하지 않게 되면 시스템 모듈이 불안정한 상태에 놓이게 되기 때문입니다. 즉, your_timer_callback에서 발생한 예외 때문에 타이머 콜백 호출 이후에 해야 하는 작업들이 진행이 되지 않을 수가 있다는 거죠. 결국은 시스템이 불안정해지는 겁니다. 이래서 Windows에서 사용하는 대부분의 콜백은 위와 같은 원리가 적용됩니다.

그렇다면 문제를 제기했던 사람이 버그를 잡기 힘들었다는 불평은 어떻게 해결해야 할까요?
간단합니다. 타이머 루틴 내부에 예외 처리 루틴을 삽입하는 겁니다.
아래와 같이 타이머 루틴을 작성하는거죠.
요롷게 만들면 자신의 루틴에서 발생한 예외는 자신이 책임질 수 있습니다.

my\_timer\_callback()  
{  
    \_\_try  
    {  
    }  
    \_\_except(EXCEPTION\_EXCUTE\_HANDLER)  
    {  
         ... 여기서 예외를 처리하세요 ...  
    }  
}

콜백 루틴은 콜백을 설정하는 모듈과(응용 프로그램), 그것을 호출해 주는 모듈이(Windows 커널) 다릅니다. 즉, 콜백 루틴에서 예외를 던져 버리면 그것을 호출한 모듈인 Windows 커널로 그 예외가 전파된다는 말입니다. 이는 서로 다른 모듈간에서 예외를 전파하는 문제가 되는 것이죠. 예외 처리에 대해서 조금만 공부하신 분들은 알겠지만 여기에는 다음과 같은 명백한 한 가지 규칙이 있습니다.

절대로 다른 모듈에 예외를 던지지 마라.
자기가 싼 똥은 자기가 치우자.

이러한 규칙을 정한 이유는 바이너리 단계에서 호환되는 예외 처리 메카니즘이 없기 때문입니다. 보통의 경우 예외처리 메카니즘은 고급 언어에서 각자 나름의 방식대로 구현해서 사용합니다. 그것들이 바이너리 단계에서 호환이 된다는 보장이 없습니다. VC++로 만든 모듈에서 던진 C++ 예외가 Visual Basic의 예외처리 메카니즘으로 자동적으로 변환되지 않는다는 이야기입니다.

물론 자신의 시스템에서 사용하는 모든 모듈에 대해서 동일한 예외 메카니즘이 사용되어 있다면 다른 모듈로 예외를 전파시켜도 상관은 없습니다. 모두 동일한 컴파일러에서 동일한 예외처리 메카니즘 옵션으로, 동일한 예외 시스템을 사용한다면 말이죠. 하지만 개발자가 저런 보장을 하기란 사실 불가능합니다. 왜냐하면 혼자서 다 만든것이 아니기 때문이죠. 서드 파티 업체의 라이브러리 하나만 들어와도 사실상 호환이 안된다고 생각해야 합니다.

결론은 질문을 하신 분의 말처럼 Windows에서 콜백의 예외를 알려주지 않고 씹는 것이 나쁜 디자인이 아니라 좋은 디자인이란 겁니다. 자신이 모듈을 설계함에 있어서도 자신의 예외가 절대 다른 모듈로 전파되지 않도록 만드는 것이 중요합니다. 특히 많이 접하게되는 DLL 경계, 스레드 경계에서 예외가 전파되지 않도록 신경을 쓸 필요가 있습니다. 반대로 자신이 다른 라이브러리의 코드를 호출할 때에도 Windows와 같이 SEH를 사용해서 해당 코드에서 발생한 시스템적 예외가 자신의 프로그램에 영향을 미치지 않도록 만드는 것도 중요합니다.

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중