여러분은 예외를 처리하고 계십니까?
여러분들은 예외에 대한 코딩을 하고 계시나요? 개발자의 성향에 따라 어떤 분은 예외 처리를 나중에 할 일로 미뤄둔 채 코딩하시는 분도 계실 것이고, 성격적인 꼼꼼함으로 인해 매 코딩마다 예외 상황으로 고려하면서 코딩을 하시는 분들도 계실 것입니다. 어떤 식으로 하시던 반드시 예외를 처리하는 것, 즉 에러가 발생할 상황을 막는 것은 꼭 필요한데요. 그렇다면, 왜 예외를 처리해야 하는 것일까요? 바보 같은 질문이겠지만 말이죠.
"무슨 말입니까! 에러가 나면 프로그램이 동작하질 않잖아요!"
맞습니다. ^^; 에러가 나면 대부분의 경우 애플리케이션이 정상적으로 동작을 하지 않죠. 사실, 그래서 에러들을 사전에 다 잡아야 하는 것입니다. 하지만, 인간은 완벽할 수 없기에 애플리케이션이 잠재적으로 일으킬 수 있는 모든 에러들을 미연에 막을 수는 없을 것입니다. 사실, 우리가 사전에 잡게 되는 에러들은 대부분 예측이 가능한 에러들이며, 기존의 사용 경험으로 인해 이미 알고 있는 에러들일 것입니다. 그리고, 이러한 에러를 잡으려고 테스트 기간을 거치면서 꼼꼼히 문제 상황들을 사전에 집어 내기도 하죠. 하지만, 우리가 예측하지 못한 에러가 발생할 확률은 여전히 존재하고 있습니다.
"애플리케이션에서 에러가 발생하는 것은 창피한 일이 아니다. 하지만, 그것을 처리하지 않고 방치해 두는 것은 정말로 부끄러운 일이다"
네. 맞습니다. '잘못을 저지르는 것은 부끄러운 일이 아니다. 잘못을 깨달은 후에도 반복하는 것이야말로 부끄러워 할 일이다'라는 장 자끄 루소의 말에 대한 패러디입니다.
애플리케이션에서 예상치 못했던 에러가 발생하는 것은 충분히 있을 수 있는 현상이기에, 중요한 것은 에러가 발생했을 경우에 어떻게 대응하는가? 얼마나 우아하게 에러가 발생한 상황을 수습할 수 있는가? 가 관건이라는 것 입니다. 해서, 이러한 에러들 즉, 사전에 모두 잡았다고 잡았는데도 불구하고 예기치 못하게 발생하는 에러들을 우리는 "처리되지 않은 예외, Unhandled Exception"이라고 이야기 합니다. 발생할 것을 예측하기 불가능했거나, 어느 정도 예측할 수는 있었지만 그 실체를 명확히 파악할 수는 없는 것들이 이에 해당합니다.
예측 불가능한 예외를 처리한다고?
그렇다면, 예측이 불가능한 데 어떻게 대응을 해야 하는 것일까요? 어떤 처리를 어떻게 해야 한다는 것인지 자못 당황스러울 수 있습니다. 그렇다면, 역으로 한번 생각해 봅시다. 예측이 불가능한 에러를 굳이 대비하고 처리해야 하는 이유는 뭘까요?
"바보에요? 아까도 말했지만, 에러가 나면 프로그램이 동작하질 않잖아요!"
그렇죠. 하지만, 앞서 말한 예외(예측 가능한 예외)의 경우와는 달리, 지금 거론하는 예기치 못한 에러는 프로그램이 처음부터 아예 동작하지 않는 것이 아니라, 특정 상황에 프로그램이 오동작하는 것이기에 사용하기에 다소 불편할 뿐, 사용을 아예 못하는 것은 아니라는 차이가 있습니다.
평상 시에 사용할 때는 문제가 발생하지 않다가, 사용자도 알 수 없는 희한한 이유로 어느 날 갑자기 애플리케이션이 오동작하는 경우가 이에 해당하니까요. 심지어는, 애플리케이션이 먹통이 되곤 하기도 하죠. 그리고, 대부분 사용자는 "어? 갑자기 이상해졌네? 왜 이러지" 하면서, 애플리케이션을 껐다가 다시 실행하겠죠? 그러면, 어? 이번엔 잘 동작합니다. 일부러 똑같은 에러를 내보려 해도 억지로 나게 하려 하면 희한하게 절대 나지 않죠. 그래서, 평소처럼 다시 사용합니다. 마치, 아주 가끔(?) IE가 먹통이 되면 작업 관리자에서 강제로 종료해 버리고 다시 실행하면서도 그다지 이상하게 생각하지 않는 것처럼 말이죠(IE 8은 좀 나아진다고 하던데, 과연 어떨지..). 다시 말해서, 예측이 불가능한 에러는 발생하더라도 평소 프로그램을 사용하는 데에는 크게 문제가 되지 않습니다. 자주 발생하지도 않고, 이유도 명확하지 않으며 특별한 상황이 아니면 그리 자주 일어나곤 하지도 않으니까요.
하지만, 이러한 에러가 잦아지게 되면, 사용자는 짜증을 낼 것이고, 다른 애플리케이션으로 옮겨갈 가능성이 높아지게 됩니다. 심성이 고운 사용자라면 해당 애플리케이션 제작 업체에게 친절하게 메일을 보내서, "이것 보세요. 오늘 지갑을 분실해서 지각하게 되었는데, 직장 상사는 자초지종도 모르면서 지각했다고 깨고, 안 그래도 짜증이 나 있는데, 당신네 프로그램을 사용하려고 하니 에러가 나네요. 이게 뭡니까!!"라고 에러가 발생하게 된 [구체적인 정황]을 설명해 줄 수도 있을 것이긴 합니다만, 대부분의 사용자는 그런 행동을 하지 않고, 그냥 기분만 상해하곤 합니다. 그리고, 주변 사람들이 추천하는 더 부드러운 애플리케이션에게로 떠나버리곤 하죠.
만일, 애플리케이션이 예상치 못한 예외를 일으키는 경우에 다음과 같이 사용자에게 안내를 해줄 수 있다면 어떨까요?
"현재 예기치 못한 문제로 에러가 발생하였습니다. 구체적인 에러의 원인은 제작사에 보고가 되었으며, 현재 처리 중에 있습니다. 불편함을 드려서 죄송합니다. 조속히 해결하도록 하겠습니다"
그리고, 에러가 발생한 내역이 실제로 제작사에게 전송이 되어서 최대한 빨리 그 문제를 해결하여 애플리케이션에 반영할 수 있다면 어떨까요? 이는 사용자에게 오히려 애플리케이션이 탄탄하게 만들어져 있고, 사후관리도 원활하게 이루어지고 있다는 느낌을 줄 수 있을 것이며, 제작사를 믿음직스럽게 할 것입니다.
즉, 이러한 예외를 처리해야 하는 이유는 떠나가는 사용자를 떠나지 않게 하기 위함이고, 애플리케이션이 예외적인 상황에서도 부드럽게 대처하고 있는 모습을 보여서 사용자의 신뢰를 얻기 위함입니다.
그렇다면, 예기치 않은 에러는 어떤 식으로 프로그래밍상에서 파악할 수 있으며, 그러한 에러를 알아냈다면 어떻게 처리를 해야 할까요? 이제 본격적으로 이를 해결하는 방법에 대해서 알아보도록 하겠습니다.
일반적인 .NET의 예외 처리
.NET에서의 예외 처리 방법으로 try.. catch… finally가 있다는 것은 굳이 거론할 필요가 없을 듯 합니다. 아마도 이 구문을 사용하여 다양하게 예외 처리를 하고 계시기도 할 것이고요. 그렇죠?
이는 에러(닷넷에서는 예외-Exception-라고 표현하죠)가 발생할 가능성이 있어 보이는 영역에 사용하여, 부드럽게 에러를 처리할 수 있게 하는 역할을 합니다. 다만, 이를 남발하면(특히나, 루프 구역 안에서 사용하면) 성능적으로 매우 좋지 않기 때문에 주의하여 사용해야 합니다. 즉, 충분히 논리적으로 문제점을 예견할 수 있는 코드에서는 try.. catch… finally 구문 보다는 If 문과 같은 제어 구문을 권장한다는 것이죠. 또한, 광범위한 코드 블록 전체를 이 구문으로 둘러싸는 것도 좋은 방법은 아닙니다.
하지만, 어디서 예외가 발생할 지 모르기에 성능적으로 좋지 않다는 것을 알면서도 에러가 의심되는 구역을 통째로 다음과 같이 try..catch 하는 경우가 있는데요. 이는 대단히 좋지 않은 방법입니다.
function void DoProcess()
{
try {
// 데이터베이스 연결하는 코드
// 쿼리를 수행해서 DataSet을 얻어오는 코드
// 결과에 따라 메일을 발송하는 코드
// 출력해야 할 구문을 템플릿을 통해 생성하는 코드
} catch (Exception ex)
{
//에러가 났을 때의 처리를 위한 공통함수 호출
}
}
그러면, 어떻게 해야 할까요? 에러가 나는 것에 대비는 해야 하겠는데, try..catch는 조심스레 사용해야 한다고 하니, 코드를 어떻게 짜야 할 지 모호하고.. 그렇지 않나요?
그렇다면, 그러한 고민을 해결해 드리겠습니다. 뚜둥!
웹 애플리케이션에서는 아예 try…catch 구문을 사용하지 마십시오. 이유는 그를 사용하지 않아도 예기치 않은 예외에 대비할 수 있는 유용한 처리 방법이 있기 때문입니다. 바로 Page_Error라는 이벤트 메서드를 사용하는 것입니다.
Page_Error 이벤트 메서드
ASPX 페이지가 실행되다가 어떤 에러가 발생한다면, 틀림없이 틀림없이 생겨난다! Page_Error 이벤트. 그렇습니다. 만일, 여러분이 Page_Error 이벤트 메서드를 작성해 두었다면, 웹 페이지(aspx)가 실행되다가 예외가 발생할 경우, 무조건 Page_Error 이벤트가 호출됩니다. 그렇기에, 이 이벤트 메서드를 작성해두면 예기치 않은 예외가 발생하는 경우에 여러분이 원하는 처리를 수행할 수 있습니다. 예를 들면, 예외 메시지를 로깅 한다거나, 관리자에게 메일을 보낸다거나, 사용자에게 부드러운 안내 메시지를 제공한다거나 하는 것들을 말이죠. 일반적으로 현업에서는 이러한 처리 모두를 수행하곤 하죠.
만일, 여러분이 공통 부모 페이지 클래스로서 PageBase라는 클래스를 만들어, 모든 웹 페이지가 이 클래스를 상속하도록 애플리케이션을 설계했다면, PageBase 클래스의 Page_Error 이벤트 메서드를 사용하여 모든 페이지에서 발생하는 예외를 중앙집중적으로 관리할 수도 있습니다. 그리고, 대부분의 웹 애플리케이션에서는 이 방법이 권장되기도 합니다. 다음은 이러한 구조로 설계된 코드 샘플입니다.
PageBase.cs의 소스
using System;
using System.Web.UI;
namespace MyWebApp.Common
{
public class PageBase : Page
{
protected void Page_Error(object sender, EventArgs e)
{
//페이지 수준에서 처리해야 할 예외들을 추출해서
//예외를 데이터베이스나 파일에 로깅한다
//혹은, 관리자에게 예러 내용을 메일로 보낸다.
}
}
}
ASPX 페이지들의 코드 비하인드
using System;
using MyWebApp.Common;
namespace MyWebApp
{
public partial class MyPage1 : PageBase
{
protected void Page_Load(object sender, EventArgs e)
{
//작업
}
}
}
다만, Page_Error 이벤트는 이름에서 보여지는 대로 웹 페이지(Page)의 라이프 사이클 내에서 발생하는 예외만을 잡을 수 있다는 단점(?)이 있습니다. 즉, 페이지 레벨에서의 예외만을 처리할 수 있고, 애플리케이션 레벨의 예외는 잡을 수 없다는 것이죠. 중, 소규모의 웹 사이트는 별도의 모듈이나 외부 컴포넌트를 사용하지 않는 단순한 구조이기에 사실상 페이지 수준의 예외만 잡아도 충분합니다. 하지만, 대규모의 사이트에서는 애플리케이션이 상당히 복잡하며, 수 많은 모듈들(HttpModule), 추가 처리기(HttpHandler), 외부 라이브러리, 컴포넌트 등이 사용될 수 있습니다. 그리고, 이들 중 일부는 사실상 페이지 라이프 사이클 외에 속해 있기에 Page_Error 이벤트만으로는 모든 예외 상황에 대비할 수가 없습니다. 예를 들면, HttpModule로 제작된 모듈에서 발생되는 예외는 Page 레벨에서 잡을 수가 없다는 것이죠.
해서, ASP.NET은 Application_Error라는 더욱 광범위한 예외 처리를 위한 장소를 제공합니다.
Application_Error
웹 페이지를 포함하여 현재의 ASP.NET 애플리케이션(페이지, 모듈, 핸들러 등을 포함)이 구동되다가 에러가 발생하게 되면 Application_Error 이벤트가 발생하게 됩니다. 이는 일반적인 에러부터 처리되지 않은 에러까지 모두가 피할 수 없이 거쳐가는 이벤트 메서드이기에 애플리케이션 수준에서 예외를 처리하고자 할 경우 대단히 유용한 처리 장소를 제공합니다. 다음 그림을 한번 참고해 보시기 바랍니다(참고로, 이 그림은 IIS 6에서의 요청 파이프라인을 보여주고 있습니다)
모든 요청은 우선 HttpRuntime을 거쳐 HttpApplication에게로 넘겨지며, HttpApplication 내부에 속해 있는 다양한 HttpModule을 수행한 다음, HttpHandler인 ASP.NET 처리기에게로 넘겨지게 됩니다. 페이지 수준의 예외 처리(Page_Error)는 HttpHandler 내부에서 발생하는 예외를 처리할 수 있는 반면, 지금 이야기하는 애플리케이션 수준의 예외 처리(Application_Error)는 페이지를 포함한 Application 전체 파이프 라인에서 발생하는 모든 예외를 처리할 수 있게 합니다(이에 대한 자세한 이야기는 http://taeyo.net/Columns/View.aspx?SEQ=97&PSEQ=8&IDX=0 를 참고하세요).
고로, 이 이벤트 메서드가 모든 예외의 중앙 관문이자, 예외 로깅을 위한 최적의 장소이기도 합니다. 그렇기에, "예외고 뭐고 난 잘 모르겠다. 그냥 다 필요 없고 한 줄 결론만 말해줘" 하시는 분들에게는 "코드에서는 예외 처리를 아무것도 하지 마시고, Global.asax에 있는 Application_Error 메서드에 필요한 예외 처리 코드를 넣으세요" 라고 말씀드리곤 합니다.
Application_Error 이벤트는 페이지에서 예외가 발생하던, 애플리케이션에서 예외가 발생하던 모든 경우에 호출되므로, 폭넓게 사용할 수 있습니다. 해서, 중,소 규모의 애플리케이션이라면 아예 Page_Error 이벤트 메서드는 작성하지 않고, Application_Error 이벤트 메서드에서 모든 예외 처리를 수행하기도 하죠.
그렇다면, 한번 예제를 통해서 Application_Error 이벤트가 얼마나 유용한지 확인해 보도록 하겠습니다. 우선, 현재의 프로젝트에 Global.asax 페이지를 하나 만들고, 비하인드 페이지에 다음과 같이 데모용 로깅 코드를 작성해 보도록 해요.
using System;
using System.IO;
using System.Web;
namespace MyWebApp
{
public class Global : System.Web.HttpApplication
{
// .. 기타 이벤트 코드들
protected void Application_Error(object sender, EventArgs e)
{
//예외를 데이터베이스나 파일에 로깅한다
Exception ex = HttpContext.Current.Server.GetLastError();
string msg = this.GetErrorMessage(ex);
msg = "\r\n-------------------------------------------------------" +
"\r\n예외 발생 일자 : " + DateTime.Now.ToString() + msg;
StreamWriter tw = File.AppendText(@"D:\temp\log.txt");
tw.WriteLine(msg);
tw.Close();
}
private string GetErrorMessage(Exception ex)
{
string err = "\r\n 에러 발생 원인 : " + ex.Source +
"\r\n 에러 메시지 :" + ex.Message +
"\r\n Stack Trace : " + ex.StackTrace.ToString();
if (ex.InnerException != null)
{
err += GetErrorMessage(ex.InnerException);
}
return err;
}
// .. 기타 이벤트 코드들
}
}
코드를 보면 대략적인 내용을 이해하실 수 있겠죠? 그렇습니다. 예외가 발생할 경우, 현재 발생한 예외(내부 예외가 있다면 그들까지 모두)의 모든 StackTrace 정보를 문자열로 구성하여 텍스트 파일에 저장하는 것입니다. 즉, 예외의 세부적인 내용을 로깅하는 것이죠.
HttpContext.Current.Server.GetLastError 라는 코드를 통해서 최종적으로 발생한 예외 개체를 얻어낼 수 있다는 것과 순환 함수로서 작성된 GetErrorMessage(..) 함수(네. 명칭은 맘에 안 드네요)를 사용하여 예외 정보를 적절한 문자열로 구성하는 것은 기억을 하시는 것이 좋겠네요.
그리고, 위의 예제 코드에서는 작성되지 않았습니다만, Application_Error 이벤트 구역 내의 코드(즉, 로깅코드)는 try..catch로 둘러싸는 것이 좋습니다. 로깅하다가 에러가 발생되면 다시 또 Applicarion_Error 이벤트가 호출되는 무한반복의 문제가 발생할 수 있으니까요(다음 강좌에서 이 부분에 대해서는 좀 더 설명드릴 예정입니다)
자. 코드가 준비되었다면 이제 강제로 예외를 일으켜서 우리의 예외 처리 방안이 잘 동작하는 지 확인해 보도록 하죠. ThrowError.aspx 라는 페이지를 하나 생성하고, 다음과 같이 강제적으로 예외를 일으키는 코드를 Page_Load에 넣어보도록 하겠습니다.
using System;
using System.Web.UI;
namespace MyWebApp
{
public partial class ThrowError : Page
{
protected void Page_Load(object sender, EventArgs e)
{
throw new Exception("강제로 예외를 어이쿠 발생시켰습니다");
}
}
}
그리고, 이 페이지를 실행(브라우저로 요청)해 보도록 하죠.
참고. 개발용 웹 애플리케이션은 반드시 가상 디렉토리로 구성합니다
물론, 여러분도 현재의 프로젝트를 가상 디렉토리로 설정하셨겠죠? 개발 시에는 반드시 그렇게 하는 것이 좋습니다. 비록 VS 가 기본적으로는 가상 IIS를 지원한다고 해도 말이죠. 그렇지 않으면, 가끔씩 IIS로 구동시킬 경우와는 다른 오동작(?)을 하는 경우가 있으므로, 꼭 가상 디렉토리 설정을 하시기 바랍니다.
혹시 아직 하지 않으셨다면, 프로젝트의 [속성] 창에 가셔서 다음과 같이 [Web] 탭에서 가상 디렉토리를 구성해 주시면 됩니다.(제 VS는 영문이라 영문 설정이 나오는 부분은 미리 죄송합니다)
페이지는 당연히 예외를 발생시킬 것입니다. 그리고, 여러분의 web.config 설정( 구역의 설정)에 따라 자세한 예외 메시지가 페이지에 나타날 수도 있고(개발 시에는 이렇게 설정해야겠죠?), 혹은 단순히 에러가 났다는 메시지만 나타날 수도 있습니다(실제 서버에서는 이렇게 나오게 해야겠죠?).
저는 실제 서버 기준으로 설정해 두었기에(즉, 으로 설정해 두었기에) 다음처럼 에러 페이지가 출력되고, 자세한 에러의 정황은 나타나지 않고 있는 것을 볼 수 있습니다.
에러가 났다면, 서버의 D:\temp\log.txt경로로 가서, 로그 파일에 자세한 에러 정보가 기록되어 있는지 확인해 보도록 합니다.
에러가 발생한 구체적인 정보가 로깅되어 있는 것을 확인할 수가 있습니다.
현재는 로그 파일명을 log.txt로 고정시켜 두었습니다만, 파일명은 날짜로 지정하여 각 날짜마다 각각의 로그 파일이 생성되도록 할 수도 있을 것입니다. 그리고, 관리자는 매일 매일 해당 날짜의 로그 파일을 열어서 예외가 발생한 내역이 있는지, 있다면 어떤 예외였는지를 파악하여 문제점을 보완할 수 있겠죠? 예외 파일이 전혀 생성되지 않는 날(로그 파일이 없으면 예외가 발생한 것이 없다는 의미이기에)을 꿈꾸며…
그리고, 좀 더 능력있는 개발자라면 저 로그를 Xml 형태로 기록한다거나, 특정 데이터베이스에 저장하도록 처리하고, 예외 로그 뷰어 같은 것을 제작한다면 보다 쉽게 예외를 관리하고 상부에 보고할 수 있을 듯도 하네요.
그렇다면, 예외 처리는 이 정도로 충분한가?
큰 줄기는 이걸로 충분하다고 봅니다만, 세세하게는 아직 다루어야 할 이야기가 조금 남아있습니다. 예를 들면, 위와 같이 중앙집중적으로 예외를 관리할 수 있게 되었다 하더라도, 여전히 try.. catch 구문을 사용할 일이 있다는 것인데요.
DAL(데이터 액세스 레이어)에서 트랜잭션을 처리할 경우 등등 에서 올바른 커밋이나 롤백을 위해 try.. catch 구문이 요구되기도 합니다. 그를 통해, 예외에 대비하는 방어적인 코드를 작성할 필요가 있다는 것이죠.
다만, 그러한 경우에도 코딩 위치에서 필요한 처리를 수행한 뒤에는 가급적 상위로 예외를 전달해주는 것이 좋습니다. 만일, 해당 부분에서 발생한 예외를 위로 전달하지 않으면, 현재의 try… catch를 거치면서 에러가 사라지게 되어 어떤 문제가 생겼는 지를 올바르게 로깅할 수 없는 상황이 연출될 수 있기 때문입니다. 모든 예외 발생 상황은 로깅이 되어야 하며, 그래야 사후 대응을 할 수 있습니다. 코드를 통한 로직으로 예외에 대비했다고 하더라도 그것이 문제 요소를 해결한 것은 아니기에 반드시 로깅을 하고, 사후에 예외가 발생하게 된 원인을 분석하여 문제 요소를 근원부터 해결할 필요가 있습니다. 사실, 이러한 이유로 런타임 예외를 로깅하는 것이죠.
또한, 지금처럼 예외 대응 코드를 global.asax 파일에 넣어두는 부분도 사실은 그다지 깔끔하다고 볼 수 없는데요. Global.asax 파일은 이 외에도 다양한 코드로 인해 충분히 복잡해질 수 있기 때문입니다. 해서, 예외 대응 코드는 별도의 HttpModule로 분리 제작하여 각 애플리케이션에서 재활용할 수 있도록 처리하는 것이 훨씬 엘레강쓰 합니다.
하여, 다음 강좌에서는 try.. catch 구문의 활용, HttpModule을 이용한 예외 관리의 모듈화 등에 대한 이야기를 이어가 볼까 합니다. ^^;