반응형

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로 분리 제작하여 각 애플리케이션에서 재활용할 수 있도록 처리하는 것이 훨씬 엘레강쓰 합니다.

authored by Taeyo


강좌 잘 보았습니다. 한가지 조심해야 할점은 Application_Error 는
Application_Error 안에서의 에러도 잡아냅니다. 만약 Application_Error 에서 에러
가 나거나 할경우 무한루프에 걸립니다.(수차례 경험....ㅠ.ㅠ) 그리고 예제 코드에
는 없지만, MSDN 에 따르면...
After handling an error, you must clear it by calling the ClearError method of
the Server object (HttpServerUtility class).

이라고 나와있습니다. Application_Error 마지막 부분에
HttpContext.Current.Server.ClearError() 요놈도 넣어주는게

반응형

+ Recent posts