Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 6. .NET 예외 처리 정리 [링크 복사], [링크+제목 복사]
조회: 28802
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

 "처리되지 않은 모든 예외는 AppDomain.UnhandledException 이벤트를 통해 잡아낼 수 있다."

기본 전제는 위와 같습니다. 위의 사항에서부터 하나씩 정리해 보도록 하겠습니다.

우선. Console Application 예를 들어볼까요?
사실 Console Application이야말로 가장 군더더기 없는 순수한 프로젝트 유형이라고 할 수 있지요. ^^

다음과 같은 코드는 Console 유형의 C# 응용 프로그램 코드입니다.

namespace ConsoleException
{
    class ExceptionClass
    {
        ~ExceptionClass()
        {
            throw new ApplicationException("Exception at Finalizer");
        }
        
    }

    class Program
    {
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.UnhandledException += delegate(object sender, UnhandledExceptionEventArgs e)
            {
                Console.WriteLine("AppDomain.CurrentDomain.UnhandledException");
            };

            {
                new ExceptionClass();
            }
            throw new ApplicationException("Exception at Main");

            // 강제로 GC를 구동. 따라서 ExceptionClass의 Finalizer가 호출됨.
            GC.Collect();

            System.Threading.ThreadPool.QueueUserWorkItem(Program.ThreadCallback);

            System.Console.Read();
        }

        static public void ThreadCallback(object state)
        {
            throw new ApplicationException("Exception at ThreadCallback");
        }
    }

}

위의 코드를 보면, 예외가 발생하는 곳이 총 3군데 있습니다. (예외 발생 순서마다 하나씩 주석 처리하시면 모든 예외를 테스트하실 수 있습니다.)

1. Finalizer에서 예외 발생
2. Main Thread에서 예외 발생
3. Thread Pool Thread에서 예외 발생

모든 예외가 동일하게 AppDomain.CurrentDomain.UnhandledException에서 걸리는 것을 확인할 수 있습니다. 일단, 콘솔 응용 프로그램만큼은 "처리되지 않은 모든 예외는 AppDomain.UnhandledException 이벤트를 통해 잡아낼 수 있다."에서 벗어나지 않는군요. ^^



그런데, 왜 WinForm과 ASP.NET은 AppDomain.UnhandledException에서 예외가 처리되지 않는 걸까요?
이 대답은, 별도의 try / catch가 처리를 하기 때문에 AppDomain.UnhandledException까지 넘어오지 않기 때문입니다. 윈폼이라고 뭐 특별한 것이 있겠습니까? ^^

그런 이유로 윈폼의 규칙을 정리해 보면,
secondary thread에서 발생하는 예외는 정상적으로 AppDomain.UnhandledException으로 이벤트가 발생되어 넘어오지만,
primary thread에서 발생하는 예외는 Application 클래스 자체에서 걸어둔 예외 핸들러로 인해 AppDomain.UnhandledException까지 넘어가지 않습니다. 대신에 자체적으로 예외가 발생했음을 알리는 Application.ThreadException 이벤트를 가지고 있습니다.

아시는 것처럼, WinForm은 직접적으로 사용자 코드를 실행시키지 않고 다음과 같이 Application에 Form 클래스를 넘겨주는 것으로 시작하는 것을 볼 수 있습니다.

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

즉, 여러분 역시 이러한 기법으로 Console Application을 만든다면, 여러분만의 프레임워크 진입 클래스를 만들어서 그 자체에서 예외를 처리할 수 있도록 만들 수 있다는 것입니다. 바로 이러한 이유로 다른 곳도 아닌, "Application" 클래스에서 "ThreadException" 이벤트를 제공해 주고 있는 것입니다.
원래 Application 클래스만 없었다면 Form1을 처리하는 primary thread에서 발생하는 예외 역시 AppDomain.UnhandledException에서 처리되었을 수 있었다는 얘기지요.

그럼,,, 좀 명확해 지셨나요?

여기서, 잠시 .NET 1.1과 .NET 2.0의 예외 처리 방식을 비교해 볼 필요가 있겠습니다.

.NET 1.1의 WinForm App에서는 secondary thread에서 예외가 발생하는 경우 AppDomain.UnhandledException에서는 try / catch를 통해서 에외를 먹어버리고 그냥 넘어가 버렸습니다.
사실, 어떻게 생각해 보면 그것이 옳았을 수도 있습니다. 이미 WinForm App는 콘솔 응용 프로그램과는 달리 상당히 복잡해진 구조를 띄고 있으며, 그렇게 덩치 큰 응용 프로그램이 2차 스레드에서 발생하는 조그만 예외 하나 처리 못했다고 죽어 버린다는 것은 용납될 수 없었기 때문일 것입니다.

하지만, .NET 2.0에서는, 명확하게 그 부분을 rethrow하고 있습니다. 즉, 2차 스레드에서의 예외 역시 응용 프로그램을 종료해버리게 만드는 구조로 바뀐 것입니다. (일례로 throw [exobject];만 한 줄 더 써주었을 것입니다.)
.NET 1.1에서 다소 느슨하게 예외처리가 되었던 WinForm Application들은 이제 .NET 2.0에 와서는 그러한 취약 부분의 코드로 인해 응용 프로그램이 종료할 수 있는 상황이 발생해 버렸습니다.

그런 경우, 우선 취할 수 있는 방법이 .NET 1.1처럼 예외를 무시하도록 만들어서 프로그램을 당장 돌아갈 수 있는 상태로 만드는 것입니다. 이를 위해 .NET 2.0에서는 다음과 같은 Config 설정으로 가능하게 하고 있습니다.

<configuration> 
  <runtime> 
   <legacyUnhandledExceptionPolicy enabled="1" /> 
  </runtime> 
<configuration> 

일단, 이렇게 만들고 난 후 정확히 어느 부분에서 예외가 발생하는지를 점검해야 할 텐데요. 간단합니다. 모든 예외는 AppDomain.UnhandledException에서 처리된다고 이미 말씀드린 것을 기억하시겠지요.
기존 코드에서 다음과 같이 UnhandledException을 처리하는 예외 핸들러를 넣고 그 안에 로그를 남기도록 구현해 줍니다.

        [STAThread]
        static void Main()
        {
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

그렇게 하나씩, secondary thread에서 예외가 발생하는 것을 없애고, 나중에는 legacyUnhandledExceptionPolicy[@enabled] 값을 다시 0으로 주거나, 아예 삭제해 버리면 됩니다.
물론, 정책에 따라서는 그냥 남겨두어도 무방하겠지요. 만약 저더러 권해 달라고 한다면, 아마 다음과 같이 말하고 싶을 것 같습니다.

"Application 개발을 릴리스해서 유지 보수를 활발히 진행할 수 있는 동안에는 legacyUnhandledExceptionPolicy[@enabled] 값을 0으로 설정해 주십시오.
그러다, 유지 보수 계약까지 완료한 이후에는 legacyUnhandledExceptionPolicy[@enabled] 값을 1로 바꿉니다. 물론 발생하는 예외에 대해서는 반드시 로그에 남겨줍니다."

위의 의견이 마음에 드세요? ^^

.NET 2.0에서 바뀐 것은 이뿐만이 아닙니다. 기존의 Application 클래스가 primary thread에서 처리하던 예외를 다시 rethrow하는 기능을 옵션으로 제공해 주도록 바뀐 것입니다. 거기서 rethrow하면 어떻게 될까요? 당연히 ^^ 그 이후에는 AppDomain.UnhandledException에서 처리를 하게 되겠지요.
일관성 있는 예외 처리라는 측면에서도 이는 바람직한 코드가 될 것 같습니다. 방식은 다음과 같습니다. (반드시 Run 메서드 호출 이전에 처리해 주어야 한다는군요.)

        [STAThread]
        static void Main()
        {
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

SetUnhandledExceptionMode의 인자 유형에 대해서는 나중에 기회되면 좀 더 다루도록 하겠습니다.



자신만의 예외 처리기를 구현한 것은 WinForm만이 아닙니다. ASP.NET 역시 Request 처리에 대해 try / catch를 걸어두고 HttpApplication 클래스에서 나름대로 처리를 하고 있습니다.
따라서, 기본 골격은 WinForm App와 비슷한 구조를 유지하고 있습니다.

Request를 처리하는 스레드에서 발생한 예외 : HttpApplication.Error
그 이외의 스레드에서 발생한 예외 : AppDomain.UnhandledException

그 이외의 구현도 ASP.NET은 WinForm의 상황과 다소 비슷합니다. .NET 1.1에서는 AppDomain.UnhandledException에서 예외를 먹어 버렸지만, .NET 2.0에서는 응용 프로그램을 종료시켜 버립니다. 이로 인해, w3wp.exe는 종료되고 IIS AppPool 관리자에 의해서 새로운 w3wp.exe가 생성되어 서비스를 시작합니다.
그것을 막기 위해 web.config에 legacyUnhandledExceptionPolicy[@enabled] 설정값을 "1"로 주는 것도 동일합니다.

다른 것이 있다면 2가지 정도가 떠오르는데요.
첫 번째, WinForm은 Main 함수에 개발자가 임의대로 코드를 주어 AppDomain.CurrentDomain.UnhandledException을 줄 수 있었지만, ASP.NET의 경우에는 그것이 불가능하다는 것입니다. 대신에 Microsoft에서는 HttpModule에서 이를 처리하도록 권고하고 있는데요. 다음과 같은 토픽에서 이에 대해 자세히 설명해 주고 있습니다.

Unhandled exceptions cause ASP.NET-based applications to unexpectedly quit in the .NET Framework 2.0
; https://docs.microsoft.com/en-us/troubleshoot/aspnet/exceptions-cause-apps-quit

두 번째는, ASP.NET에서는 WinForm의 Application.SetUnhandledExceptionMode와 같은 기능은 지원하지 않는다는 것입니다. 따라서 WinForm처럼, HttpApplication에서의 예외 처리를 AppDomain.UnhandledException에 넘겨서 예외를 일괄 처리할 수 있는 방법은 없습니다.
ASP.NET 2.0에서 반가운 소식이 한 가지 있다면, 이제 Request 처리 시에 발생하는 예외에 대해서는 Windows Event Log에 상세하게 로그 기록이 남겨진다는 것입니다. 반면에 아쉬운 소식이 있다면, ^^ 그 이외의 thread에서 발생하는 예외의 경우에는 역시 이벤트 로그에 남겨주기는 하지만 그다지 상세한 로그를 남겨주지는 않는다는 점입니다. 아래는 실제로 별도의 스레드에서 예외가 발생했을 때 남겨진 이벤트 로그 내용입니다.

Event Type:	Error
Event Source:	.NET Runtime 2.0 Error Reporting
Event Category:	None
Event ID:	5000
Date:		8/9/2006
Time:		11:13:49 AM
User:		N/A
Computer:	SEDONA
Description:
EventType clr20r3, P1 w3wp.exe, P2 6.0.3790.1830, P3 42435be1, P4 app_web_0mz2jpmu, 
P5 0.0.0.0, P6 44d9448b, P7 4, P8 b, P9 system.applicationexception, P10 NIL.


For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.
Data:
0000: 63 00 6c 00 72 00 32 00   c.l.r.2.
0008: 30 00 72 00 33 00 2c 00   0.r.3.,.
;
[중간 생략]
;
00e0: 6f 00 6e 00 20 00 4e 00   o.n. .N.
00e8: 49 00 4c 00 0d 00 0a 00   I.L.....

이보다 더 상세한 로그를 원하신다면, 위의 첫 번째 방법에서 소개했던 "https://docs.microsoft.com/en-us/troubleshoot/aspnet/exceptions-cause-apps-quit?WT.mc_id=DT-MVP-4038148"에서 제시하는 방법을 사용하면 가능합니다.



[내용추가]
2006.09.07:
위의 내용 중에서 제가 실수를 한 것이 있습니다. legacyUnhandledExceptionPolicy 요소에 대한 설정을 하는 부분에서 그것을 web.config에 해야 한다고 써놓았는데, web.config이 아닌 "%WINDIR%\Microsoft.NET\Framework\v2.0.50727\Aspnet.config" 파일에 넣어두어야 합니다.


2006.09.20 : [내용 추가] ASP.NET 관련해서 예외 처리 정리를 다음의 토픽에 실었으니 참고하십시오.
CLR & Debug Features : 3.14.1 ASP.NET 디버깅 환경 구성
; https://www.sysnet.pe.kr/2/0/342



[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]
[연관 글]






[최초 등록일: ]
[최종 수정일: 5/6/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2008-11-09 12시58분
WPF - 전역 예외 처리
; http://www.sysnet.pe.kr/2/0/614
kevin25
2008-11-09 12시59분
Watson Bucket 정보를 이용한 CLR 응용 프로그램 예외 분석
; http://www.sysnet.pe.kr/2/0/595
kevin25
2013-01-30 02시22분
정성태
2014-12-27 03시05분
[Ari] 정말 피가되고 살이되는 한글 도움글 정말 감사합니다.
[손님]
2017-06-12 11시28분
정성태

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13184정성태12/5/202232개발 환경 구성: 652. ml64.exe와 link.exe x64 실행 환경 구성
13183정성태12/4/202226오류 유형: 830. MASM + CRT 함수를 사용하는 경우 발생하는 컴파일 오류 정리
13182정성태12/4/202257Windows: 217. Windows 환경에서의 Hello World x64 어셈블리 예제 (MASM 버전)
13181정성태12/3/202254Linux: 54. 리눅스/WSL - hello world 어셈블리 코드 x86/x64 (nasm)
13180정성태12/2/202260.NET Framework: 2074. C# - 스택 메모리에 대한 여유 공간 확인하는 방법파일 다운로드1
13179정성태12/2/202232Windows: 216. Windows 11 - 22H2 업데이트 이후 Terminal 대신 cmd 창이 뜨는 경우
13178정성태12/1/2022114Windows: 215. Win32 API 금지된 함수 - IsBadXxxPtr 유의 함수들이 안전하지 않은 이유파일 다운로드1
13177정성태11/30/202267오류 유형: 829. uwsgi 설치 시 fatal error: Python.h: No such file or directory
13176정성태11/29/202256오류 유형: 828. gunicorn - ModuleNotFoundError: No module named 'flask'
13175정성태11/29/202279오류 유형: 827. Python - ImportError: cannot import name 'html5lib' from 'pip._vendor'
13174정성태11/28/2022106.NET Framework: 2073. C# - VMMap처럼 스택 메모리의 reserve/guard/commit 상태 출력파일 다운로드1
13173정성태11/27/2022196.NET Framework: 2072. 닷넷 응용 프로그램의 스레드 스택 크기 변경
13172정성태11/25/2022243.NET Framework: 2071. 닷넷에서 ESP/RSP 레지스터 값을 구하는 방법파일 다운로드1
13171정성태11/25/2022201Windows: 214. 윈도우 - 스레드 스택의 "red zone"
13170정성태11/24/2022347Windows: 213. 윈도우 - 싱글 스레드는 컨텍스트 스위칭이 없을까요?
13169정성태11/23/2022362Windows: 212. 윈도우의 Protected Process (Light) 보안 [1]파일 다운로드2
13168정성태11/22/2022317제니퍼 .NET: 31. 제니퍼 닷넷 적용 사례 (9) - DB 서비스에 부하가 걸렸다?!
13167정성태11/21/2022312.NET Framework: 2070. .NET 7 - Console.ReadKey와 리눅스의 터미널 타입
13166정성태11/20/2022240개발 환경 구성: 651. Windows 사용자 경험으로 WSL 환경에 dotnet 런타임/SDK 설치 방법
13165정성태11/18/2022252개발 환경 구성: 650. Azure - "scm" 프로세스와 엮인 서비스 모음
13164정성태11/18/2022410개발 환경 구성: 649. Azure - 비주얼 스튜디오를 이용한 AppService 원격 디버그 방법
13163정성태11/17/2022261개발 환경 구성: 648. 비주얼 스튜디오에서 안드로이드 기기 인식하는 방법
13162정성태11/15/2022647.NET Framework: 2069. .NET 7 - AOT(ahead-of-time) 컴파일
13161정성태11/14/2022487.NET Framework: 2068. C# - PublishSingleFile로 배포한 이미지의 역어셈블 가능 여부 (난독화 필요성) [2]
13160정성태11/11/2022567.NET Framework: 2067. C# - PublishSingleFile 적용 시 native/managed 모듈 통합 옵션
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...