Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 6. .NET 예외 처리 정리 [링크 복사], [링크+제목 복사]
조회: 34114
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 8개 있습니다.)
(시리즈 글이 11개 있습니다.)
디버깅 기술: 6. .NET 예외 처리 정리
; https://www.sysnet.pe.kr/2/0/316

디버깅 기술: 15. First-Chance Exception
; https://www.sysnet.pe.kr/2/0/510

디버깅 기술: 16. Watson Bucket 정보를 이용한 CLR 응용 프로그램 예외 분석
; https://www.sysnet.pe.kr/2/0/595

.NET Framework: 110. WPF - 전역 예외 처리
; https://www.sysnet.pe.kr/2/0/614

디버깅 기술: 42. Watson Bucket 정보를 이용한 CLR 응용 프로그램 예외 분석 - (2)
; https://www.sysnet.pe.kr/2/0/1096

.NET Framework: 534. ASP.NET 응용 프로그램이 예외로 프로세스가 종료된다면?
; https://www.sysnet.pe.kr/2/0/10863

.NET Framework: 538. Thread.Abort로 인해 프로세스가 종료되는 현상
; https://www.sysnet.pe.kr/2/0/10867

디버깅 기술: 110. 비동기 코드 실행 중 예외로 인한 ASP.NET 프로세스 비정상 종료 현상
; https://www.sysnet.pe.kr/2/0/11383

디버깅 기술: 119. windbg 분석 사례 - 종료자(Finalizer)에서 예외가 발생한 경우 비정상 종료(Crash) 발생
; https://www.sysnet.pe.kr/2/0/11732

닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
; https://www.sysnet.pe.kr/2/0/13422

닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
; https://www.sysnet.pe.kr/2/0/13551





 "처리되지 않은 모든 예외는 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://learn.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://learn.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



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






[최초 등록일: ]
[최종 수정일: 2/8/2024]

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] 정말 피가되고 살이되는 한글 도움글 정말 감사합니다.
[guest]
2017-06-12 11시28분
정성태
2006-09-20 12시06분
Asynchronous operations, pinning
; https://learn.microsoft.com/en-us/archive/blogs/cbrumme/asynchronous-operations-pinning

위의 토픽을 읽고 나니, ".NET Exception" 처리에 대해서 다시 한번 생각하게 됩니다. 갑자기 일관성을 잃어버린 듯한 느낌이 드는군요.

다들 아시는 것처럼, 비동기 처리를 위해서 "Delegate"의 "BeginInvoke" 함수를 사용하는 경우가 있는데요. 해당 함수에서 "예외"가 발생하면, 그런 경우에는 예외를 먹어버리고 있습니다. 예를 들어, 다음과 같은 코드를 들 수 있는데요.


private void Form1_Load(object sender, EventArgs e)
{
  TestAsyncMethod testMethod = new TestAsyncMethod(this.testMethod);
  testMethod.BeginInvoke(null, null, null);
}

private void testMethod(object objState)
{
  throw new ApplicationException();
}


위의 예에서, 분명히 testMethod는 ThreadPool에서 제공되는 secondary thread를 사용함에도 불구하고 예외가 발생한 것을 무시해 버리고 있습니다. 물론, ThreadPool.QueueUserWorkItem이나 별도 스레드 생성으로 testMethod를 실행하면 예외로 인해 프로세스가 죽게 됩니다.

어쨌든 위에서 소개해 드린 토픽에서는 BeginXXX으로 인한 호출을 하는 경우에는 반드시 짝을 이뤄 EndXXX 메서드를 호출하라고 권장하고 있으며, EndXXX 메서드 호출 시에 비동기로 실행된 메서드에서 발생한 예외까지 throw를 해준다고 합니다.
정성태

... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13221정성태1/19/20234162Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/20234313오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/20233871Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/20233794VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/20234389디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/20234653디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
13215정성태1/11/20236158Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법 [1]
13214정성태1/10/20235727.NET Framework: 2087. .NET 6부터 SourceGenerator와 통합된 System.Text.Json [1]파일 다운로드1
13213정성태1/9/20235267오류 유형: 836. docker 이미지 빌드 시 "RUN apt install ..." 명령어가 실패하는 이유
13212정성태1/8/20235028기타: 85. 단정도/배정도 부동 소수점의 정밀도(Precision)에 따른 형변환 손실
13211정성태1/6/20235111웹: 42. (https가 아닌) http 다운로드를 막는 웹 브라우저
13210정성태1/5/20234132Windows: 219. 윈도우 x64의 경우 0x00000000`7ffe0000 아래의 주소는 왜 사용하지 않을까요?
13209정성태1/4/20234026Windows: 218. 왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?
13208정성태1/3/20233961.NET Framework: 2086. C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법파일 다운로드1
13207정성태12/26/20224270.NET Framework: 2085. C# - gpedit.msc의 "User Rights Assignment" 특권을 코드로 설정/해제하는 방법파일 다운로드1
13206정성태12/24/20224474.NET Framework: 2084. C# - GetTokenInformation으로 사용자 SID(Security identifiers) 구하는 방법 [3]파일 다운로드1
13205정성태12/24/20224870.NET Framework: 2083. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용 (2)파일 다운로드1
13204정성태12/22/20224152.NET Framework: 2082. C# - (LSA_UNICODE_STRING 예제로) CustomMarshaler 사용법파일 다운로드1
13203정성태12/22/20224284.NET Framework: 2081. C# Interop 예제 - (LSA_UNICODE_STRING 예제로) 구조체를 C++에 전달하는 방법파일 다운로드1
13202정성태12/21/20224659기타: 84. 직렬화로 설명하는 Little/Big Endian파일 다운로드1
13201정성태12/20/20225285오류 유형: 835. PyCharm 사용 시 C 드라이브 용량 부족
13200정성태12/19/20224161오류 유형: 834. 이벤트 로그 - SSL Certificate Settings created by an admin process for endpoint
13199정성태12/19/20224449개발 환경 구성: 656. Internal Network 유형의 스위치로 공유한 Hyper-V의 VM과 호스트가 통신이 안 되는 경우
13198정성태12/18/20224325.NET Framework: 2080. C# - Microsoft.XmlSerializer.Generator 처리 없이 XmlSerializer 생성자를 예외 없이 사용하고 싶다면?파일 다운로드1
13197정성태12/17/20224267.NET Framework: 2079. .NET Core/5+ 환경에서 XmlSerializer 사용 시 System.IO.FileNotFoundException 예외 발생하는 경우파일 다운로드1
13196정성태12/16/20224396.NET Framework: 2078. .NET Core/5+를 위한 SGen(Microsoft.XmlSerializer.Generator) 사용법
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...