Microsoft MVP성태의 닷넷 이야기
디버깅 기술: 6. .NET 예외 처리 정리 [링크 복사], [링크+제목 복사]
조회: 34108
글쓴 사람
정성태 (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를 해준다고 합니다.
정성태

1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...
NoWriterDateCnt.TitleFile(s)
13294정성태3/22/20234122.NET Framework: 2105. LargeAddressAware 옵션이 적용된 닷넷 32비트 프로세스의 가용 메모리 - 두 번째
13293정성태3/22/20234191오류 유형: 853. dumpbin - warning LNK4048: Invalid format file; ignored
13292정성태3/21/20234305Windows: 232. C/C++ - 일반 창에도 사용 가능한 IsDialogMessage파일 다운로드1
13291정성태3/20/20234712.NET Framework: 2104. C# Windows Forms - WndProc 재정의와 IMessageFilter 사용 시의 차이점
13290정성태3/19/20234219.NET Framework: 2103. C# - 윈도우에서 기본 제공하는 FindText 대화창 사용법파일 다운로드1
13289정성태3/18/20233416Windows: 231. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 자식 윈도우를 생성하는 방법파일 다운로드1
13288정성태3/17/20233515Windows: 230. Win32 - 대화창의 DLU 단위를 pixel로 변경하는 방법파일 다운로드1
13287정성태3/16/20233683Windows: 229. Win32 - 대화창 템플릿의 2진 리소스를 읽어들여 윈도우를 직접 띄우는 방법파일 다운로드1
13286정성태3/15/20234146Windows: 228. Win32 - 리소스에 포함된 대화창 Template의 2진 코드 해석 방법
13285정성태3/14/20233736Windows: 227. Win32 C/C++ - Dialog Procedure를 재정의하는 방법파일 다운로드1
13284정성태3/13/20233936Windows: 226. Win32 C/C++ - Dialog에서 값을 반환하는 방법파일 다운로드1
13283정성태3/12/20233478오류 유형: 852. 파이썬 - TypeError: coercing to Unicode: need string or buffer, NoneType found
13282정성태3/12/20233807Linux: 58. WSL - nohup 옵션이 필요한 경우
13281정성태3/12/20233716Windows: 225. 윈도우 바탕화면의 아이콘들이 넓게 퍼지는 경우 [2]
13280정성태3/9/20234453개발 환경 구성: 670. WSL 2에서 호스팅 중인 TCP 서버를 외부에서 접근하는 방법
13279정성태3/9/20233997오류 유형: 851. 파이썬 ModuleNotFoundError: No module named '_cffi_backend'
13278정성태3/8/20233944개발 환경 구성: 669. WSL 2의 (init이 아닌) systemd 지원 [1]
13277정성태3/6/20234576개발 환경 구성: 668. 코드 사인용 인증서 신청 및 적용 방법(예: Digicert)
13276정성태3/5/20234306.NET Framework: 2102. C# 11 - ref struct/ref field를 위해 새롭게 도입된 scoped 예약어
13275정성태3/3/20234660.NET Framework: 2101. C# 11의 ref 필드 설명
13274정성태3/2/20234252.NET Framework: 2100. C# - ref 필드로 ref struct 타입을 허용하지 않는 이유
13273정성태2/28/20233953.NET Framework: 2099. C# - 관리 포인터로서의 ref 예약어 의미
13272정성태2/27/20234199오류 유형: 850. SSMS - mdf 파일을 Attach 시킬 때 Operating system error 5: "5(Access is denied.)" 에러
13271정성태2/25/20234142오류 유형: 849. Sql Server Configuration Manager가 시작 메뉴에 없는 경우
13270정성태2/24/20233754.NET Framework: 2098. dotnet build에 /p 옵션을 적용 시 유의점
13269정성태2/23/20234289스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
1  2  3  4  5  6  7  8  9  10  11  12  [13]  14  15  ...