Right now do !

[C#기초]응용 프로그램의 예외 처리-2편 Unhandled Exception 처리

by 지금당장해

 응용 프로그램의 예외 처리-1편의 마지막에 독자에게 던진 질문이 "Main함수에서 조차 예외를 throw를 한다면 또는 애시당초 그 어디에서도 예외를 헨들링 하지 않는다면 그 예외는 어떻게 될까?"였다. 이런 예외를 Unhandled Exception이라고 부른다. 누구도 헨들링(처리 하지 않은 오류) 이런 예외는 다양한 방법으로 사용자에게 불안감 불쾌감으로 다가온다. 예외가 발생한 프로그램의 종류에 따라 혹은 발생한 오류에 따라 이 예외라는 녀석의 표현은 다양하다.

 이 시점에서 .NET으로 작성 할 수 있는 프로그램 종류를 정리 해보자. Visual Studio에서 생성할 수 있는 프로젝트중 클래스 라이브러리를 제외한 모든 프로젝트가 프로그램의 종류라고 볼 수 있다. 작금의 .NET은 프레임워크가 전통적인 .NET Framework와 Core 그리고 Standard등으로 나누어 볼 수 있어 그 종류는 상당하다. 이번 글에서 전통적인 .NET Framework에 한정하여 내용을 정리하고자 한다. (다른 .NET은 필자가 아직 실 프로젝트에서 다루지 못해본 관계로 아직 논할 자격이 없다고 생각한다.) 그렇게 봤을때 우리가 논해볼 수 있는 .NET의 프로그램 종류는 다음과 같다. 


1) WinForm이라고 부르는 Windows Forms 프로그램 


2) Window Service 프로그램 


3) ASP.NET Web 프로그램





 이상 세 가지로 나누어 볼 수 있다. 여기에 추가로 고려해 봐야 하는 프로그램의 형태중 하나가 WPF Windows 응용프로그램 인데 이는 예외 처리 관점에서는 WinForm과 동일하다고 볼 수 있어 별도로 구분 짖지 않도록 하겠다. 

이 3가지 구분은 시스템에서 발생한 예외를 마주해야 하는 사용자의 관점이다. 

첫 번째 WinForm에서 발생하는 예외의 대부분은 사용자와 프로그램의 UI의 상호작용 중에 발생하는 것이다. 사용자는 UI를 통해 프로그램에게 어떤 명령을 내리고 그에 상응하는 정상 처리를 기대한다. 이때 프로그램의 예외는 프로그래머가 의도치 못한 입력 값 또는 그가 작성한 프로그램의 오류, 운영환경의 H/W 또는 S/W시스템에 기인한다. 따라서 WinForm 프로그램에서 발생한 예외는 이 프로그램을 사용하는 사용자에게 "당신의 명령이 이런 이런 이유로 정상적으로 완료되지 못했습니다." 라고 즉시 알려야 한다.

 이와 반대로 두 번째에서 나열한 프로그램의 형태인 Window Service 프로그램의 경우에는 UI를 가지고 있지 않다. .NET으로 구현되어 있지는 않았으나 우리가 접할 수 있는 대표적인 Service프로그램 형태인 DBMS를 예로 들어 보겠다. DBMS 운영중에 디스크가 Full되어 있는 상태인데 사용자가 Insert하는 명령을 수행 시켰다고 가정 해보자. SQL문장에는 아무런 문제가 없다. 그런데 없는 디스크를 어쩌란 말인가? 시스템 입장에서 이 상황을 어떻게 사용자에게 알려야 할까? JDBC 또는 ODP.NET과 같은 DB Provider를 통해 예외를 던진다. 상황을 자세히 기술하여 이를 이용하고 있는 응용프로그램에 전달 해야 한다. 헌데 개발자가 게을러 DB와 상호작용 속에 발생 할 수 있는 예외 처리를 하지 않은 경우는 어찌 해야할까? 이 부분은 어찌 할 수 없다. 전편 글과 같이 야구를 예를 들어 설명하겠다. 투수가 던진 볼이 타자 베트에 맞아 외야 로 볼이 날아가 땅볼이된 예외가 발생 했고, 외야수는 이를 처리하기 위해 포수 방향으로 던졌는데 포수가 뒤에 있던 심판과 노닥거리면 게으름을 피우다가 날라오는 볼을 외면한것이다. 이 상황을 지켜보던 투수는 포수의 뒷통수를 후려치고 싶은 마음이 굴뚝 같으나 먼저 포수 언저리에서 뒹굴고 있는 볼을 잡아 베이스를 향해 달려오는 주자를 저지 하려 할 것이다. 시스템에서도 이런 일이 처리된다. 




DBMS는 예외를 응용프로그램에게 알리기도 하지만 스스로가 로그라고 하는 장치를 통해 이런 상황을 기록해둔다. 야구에서도 이런 상황을 포수 실책 이라고 기록하는 것으로 알고 있다. 적어도 시스템 관리자가 외 사용자가 등록한 값이 DB에 입력되지 못했음을 찾을수 있어야 하고 이를 게으른 응용 프로그램 개발자에게 알릴수 있어야 하기 때문이다. 이렇게 머리는 있으나 입이 없는 Service 프로그램은 이런 방법으로 예외를 처리 해야한다.

 ASP.NET을 마지막에 설명하고자 하는 이유는 이렇다. 두 가지 성격을 모두 가지고 있는 프로그램이기 때문이다. 글을 읽는 독자중 일부는 "여보셔 필자 ASP.NET은 엄연히 브라우저를 통하긴 하지만 HTML이라고 하는 얼굴이 있지 않소?"라고 반문 할 것이다. "맞소!" 맞고요 ... 그리고 ASP.NET은 Web Service를 가지고 있기 때문에 때로는 얼굴이 없다고 해야 한다. Window Service가 로컬의 서비스라면 Web Service는 원격의 Service성격을 가지고 있기 때문이다. 따라서 ASP.NET에서 처리해야 하는 예외의 자세한 설명은 각 응용프로그램 별 예외처리 편에서 설명 하도록 하고 원래 하려고 했던 Unhandled Exception에 집중 해보도록 하겠다.

 

 다시 야구를 예를 들어 설명을 이어가자면 앞에서는 그래도 투수가 정신을 차리고 있다가 투수의 게으름을 방어 했으니 엄밀히 이야기 하면 Unhandled된 상황은 아니라고 본다. 진정한 Unhandled는 투수가 예외 처리를 하지 않고 본인 감정을 못이겨 포수의 뒷통수를 치고 있는 상황이다. (실제 이런 상황이 벌어지면 맥주한잔 하신 아저씨가 구장에 난입하여 윗통을 벗고 행패를 부리고도 남았겠지만...) 시스템은 야구장이 아니다. OS던 Framework이던 이런 상황을 대응해야 한다. .NET은 특수한 상황이 아니라면 Framework Layer에서 이런 무책임(Unhandled)한 상황을 책임 져준다. 굳이 비유하자면 포수의 뒷통수를 치고 있는 투수를 말려 경기에 임하게 하는 심판의 역할 이다.


                                                                                                    <닷넷 심판의 모습>

위와 같은 화면을 봤다면 독자가 사용하고 있는 "프로그램은 닷넷으로 작성되었음과 그 프로그램을 작성한 개발자가 상당히 게을러 Unhandled Exception 대응 코드가 없음" 2가지를 동시에 증명하는 것이다. 


<ASP.NET에서 처리되지 않은 예외>


 혹자는 위 그림을 보고 "훌륭하네 이렇게 보여주면 되지 뭐"라고 생각할 수 도 있다. 그러나 위 와같은 예외 처리 UI를 개발자가 아닌 일반 사용자가 봤을때 느끼는 감정 (서두에서 이야기 했던 불안감, 불쾌감 이런거) 별로 좋지 않다. 사실 아무리 순화 시켜서 표현을 해도 "시스템에 예기치 않은 오류가 발생 하였습니다."를 본다면 사용자는 바로 전화를 들어 시스템 관리자에게 본인이 30분 이상 입력한 값이 날아 갔음에 대한 불만을 표출하겠지만 최소한의 개발자의 양심이 있다면 이대로 사용자에게 이런 모습의 오류 발생을 알리는 것은 정말 아니다. 또 다른 문제를 나열 해보면 사용자가 만약 자세히를 눌러 오류의 원인을 보면 해당 예외가 발생하기 까지의 모든 호출 흐름이 그야말로 자세히 표출된다. 이는 때에 따라서 심각한 보안 이슈를 일으킬수 있다. 실제로 필자가 알고있는 대다수의 대기업 보안 심사 조건에 예외 발생시 이 호출흐름을 사용자에게 노출 시키지 못하도록 가이드 하고 있다. 이 대화상자에서 사용자가 계속을 누르면 프로그램을 그야 말로 계속 쓸수 있게 한다. 그랬을때 초래되는 2차적인 부작용을 상상 해보자. (실제 있었던 일이기도 하다.) 창이 Load될때 조회된 정보를 기반으로 추가 사용자 입력을 하고 최종 등록 버튼을 눌러 저장하는 흐름의 사나리오를 가지고 있는 단위 프로그램이 있다고 가정 한다. 창 Load시 조회 작업중 예외가 발생 하였는데 사용자가 계속을 눌렀고 본인이 입력할 값을 Key-in하고 저장 버튼을 눌렀다. DB Table에는 Load시 조회 되었어야 할 사전 정보에 해당하는 필드가 Nullable이고 응용프로그램 Layer의 그 어떤 구간에서도 이 값이 비어 있음을 확인하는 구문이 없었다고 상상 해보자. 그렇다 필자가 이야기 하고 싶은 이야기는 Unhandled Exception상황에서는 통상 프로그램을 종료 시켜야 한다는 것이다. 물론 사용자가 "끝내기"를 선택하였다면 같은 효과이다. 허나 이를 기다하는 것은 고양이에게 키보드를 맞기는것고 같다. (개발자는 보수적이다 못해 염세적이어야 발을 뻣고 잠을 잘수가 있다. 사용자가 알아서 하겠지 하는 안이()한 생각을 계속 할꺼면 여러사람 괴롭히지 말고 빨리 다른일을 알아봐야 한다.) 


 결론은 아주 간단하다. "처리되지 않은 예외를 처리하라!!!" 이 앞뒤가 안맞는 듯한 명제가 개발자가 해야 할 일이다. 이는 위에서 이야기 한 프로그램의 형태에 따라 상이 하다. 먼저 Windows Forms에서의 처리를 보자.

using System;
using System.Windows.Forms;
using CefSharp.WinForms;

static class Program    
{
  /// 
  /// 해당 응용 프로그램의 주 진입점입니다.
  /// 

[STAThread] static void Main() { Application.ThreadException += Application_ThreadException; Application.SetUnhandledExceptionMode(UnhandledExceptionMode.Automatic); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; // throw new Exception("UI 생성되기 전에 오류를 throw 했습니다."); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) { System.Diagnostics.Trace.WriteLine("Application_ThreadException " + e.Exception.Message); MessageBox.Show("Application_ThreadException " + e.Exception.Message); Application.Exit(new System.ComponentModel.CancelEventArgs(false)); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { System.Diagnostics.Trace.WriteLine("CurrentDomain_UnhandledException " + ((Exception)e.ExceptionObject).Message); MessageBox.Show("CurrentDomain_UnhandledException " + ((Exception)e.ExceptionObject).Message + " Is Terminating: " + e.IsTerminating.ToString()); } }


Winform Main 응용프로그램을 지칭하는 Application static객체에 ThreadException이라고 하는 예외 처리용 이벤트에 Application_ThreadException 함수를 바인딩하였다. 이는 해당 Winform 프로그램의 주 Thread역할을 하는 UI Thread 내에서 발생하는 오류를 받아 처리 하겠다고 선언하는 것이다. 즉  Form1이라고 하는 윈도우 폼 Class또는 이 폼에서 생성하는  코드내에서 발생하는 처리 되지 않는 오류가 있다면 Application_ThreadException 함수가 처리 하겠다는 의미가 된다. 

private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) 함수를 살펴 보자 인자로 전달된 e는 예외를 내포하고 있어 상기 코드와 같이 예외 메시지를 추출 할 수 있다. 이 예외 객체를 통해 사용자에게 오류를 알리고 시스템 여기저기에 로깅을 하고 마지막에 개발자가 결정해야 하는 것이 프로그램 종료를 결정 지어야 한다. 바로 Application.Exit(new System.ComponentModel.CancelEventArgs(false));이 코드다. new System.ComponentModel.CancelEventArgs(false)이 인자를 생략하거나 인자를 false로 설정하여 넘기면 프로그램이 종료되고 반대로 true로 설정하면 프로그램은 종료되지 않고 위에서 언급한 "계속"이 되어버린다. 


 위 코드에서 주석으로 처리된 throw new Exception("UI 생성되기 전에 오류를 throw 했습니다.");  예외 던지기를 수행 하면 어떻게 될까? 이 단계에서는 UI thread가 생성되기 전이므로 Application_ThreadException함수가 예외를 처리 하지 못한다. 이렇게 UI Thread외에서 발생한 예외를 처리 하기 위해 AppDomain.CurrentDomain.UnhandledException이벤트 처리 함수를 구현한다. 이 의미는 "현재 응용프로그램 도메인 안에서 발생한 오류를 잡겠다."이다. (응용프로그램 도메인의 개념에 대해서 논하면 더 길어지니까 나중에 별도로 다루겠다.) 헌데 이 이벤트의 구현 함수인 CurrentDomain_UnhandledException에는 Application.Exit()함수 호출이 없다. 필자가 빼 놓은것이 아니다. 인자로 넘어온 e 내부 객체에 IsTerminating라고 하는 bool속성이 true로 넘어오면 프로그램은 자동 종료된다. 필자 경험상 여기에 false가 넘어온 경우는 아직 못 봤다. 외 이번엔느 프로그램이 알아서 종료되는 것일까? 필자 추정인데 UI가 아닌 Thread에서 오류가 났는데 이를 사용자에게 알려 계속 할지 말지 알릴 방법도 없고 처리도 애매 해서 아닐까 싶다. (MS 특유의 친절함) 


이 글을 읽는 독자의 푸념이 들려온다. "아~ UI Thread는 Application_ThreadException에서 처리 하고 나머지는 CurrentDomain_UnhandledException에서 처리하고 좀 그렇다." 친절한 MS씨는 이 불평도 처리 할 수 있다. 이런 욕구가 있다면 Application.SetUnhandledExceptionMode()함수를 이용하자. 인자로 위와 같이 Automatic이 아니라 UnhandledExceptionMode.ThrowException를 설정하면 된다. UI Thread에서는 예외를 처리 하지 않고 도메인을 넘기겠다는 의미로 해석하면 된다. 자세한 사항은 아래 URL을 참조 할 것.


https://docs.microsoft.com/ko-kr/dotnet/api/system.windows.forms.application.setunhandledexceptionmode?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev15.query%3FappId%3DDev15IDEF1%26l%3DKO-KR%26k%3Dk(System.Windows.Forms.Application.SetUnhandledExceptionMode);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv3.5);k(DevLang-csharp)%26rd%3Dtrue&view=netframework-4.7.2



사용자에게 예외의 발생을 알리고 프로그램을 종료 시킬지 개발자 또는 사용자가 결정하게 하는 WinForm예외 처리에 기본적인 사항외에 오류가 발생한 정확한 원인 분석을 위해 Message와 Stack trace를 시스템에 로그로 남기는 개발자의 치밀함도 때에 따라서 필요하다. 이 부분 까지 설명하는 것은 이 제목에 본질에서 좀 벗어 나는것 같아서 독자에게 맞겨 두기로 하고 이번 글은 여기까지 하겠다. 


다음글은 Service와 ASP.NET의 예외 처리를 정리하여 이 연재를 마무리 하려 한다.

블로그의 정보

지금 당장 해!!!

지금당장해

활동하기