ASP.NET HTTP 런타임은 개제 지향적이고, 이벤트 지향적인 하부구조를 노출하고 있기에, 요청과 응답 사이에 참여하는 어플리케이션을 쉽게(?) 작성할 수 있게 해 줍니다. 즉, ASP.NET 런타임의 여러 프로세스 사이에 우리가 제작한 특별한 기능들을 끼워 넣어 구동시킬 수 있다는 것이죠. 그리고, 그것을 가능하게 하는 것 중 하나가 이번 강좌에서 다룰 HttpModule 입니다.

디자인 패턴의 측면에서 바라보자면, HttpModule은 Interceptiong Filter 패턴의 한 유형이라 할 수 있습니다. 요청의 처리 전과 후에 서버의 처리 이벤트들을 가로채어서 추가적인 어떤 로직을 수행할 수 있게 하는 패턴을 일반적으로 Interceptiong Filter 패턴이라고 하는데, HttpModule가 바로 그러한 역할을 수행하는 필터이기 때문입니다. .NET 이전에는 이러한 필터를 제작하기 위해서 C++로 ISAPI 필터를 제작해야 했습니다. 그렇기에, HTTP 모듈은 이전의 ISAPI(Internet Services Application Programming Interface) Filter의 .NET 입장의 논리적인 대체라고 볼 수 있습니다.

HttpModule의 역할 및 실행 시점 등에 대해서는 이전 강좌에서 어느 정도 설명이 되었으므로, 이제는 실제적인 이야기로 들어가 보도록 하겠습니다. 조금은 어려울 수도 있는 이야기지만, 이야기 재주꾼 태오의 쉬운 설명이 있으니 결코 겁나지 않습니다!!! 그렇지 않습니까? -_-+ (역시 그렇지 않군요!)

우선 여러분은 기존의 글들 및 위의 글들을 읽으면서 이렇게 생각하셨을 가능성이 매우 큽니다.

"그래서, HttpModule을 만들어서 어디다가 쓰겠다는 거야? 난 아직도 HttpModule라는 것이 무엇인지, 왜 필요한지, 그리고 어디에 적용해야 하는 것인지 감이 도통 오지 않을라구 해! 지금껏 그거 몰랐어도 ASP.NET으로 프로그램 짜는데 아무런 프라블럼이 없었다구~"

그렇습니다. 그렇다면 이제 말입니다. 저도 잘 모르면서 아는 척 그만하고, 실제로 이해를 도울만한 이야기를 해야 할 듯 합니다. 안 그랬다간, “그래, 네 덩 굵다!”란 지탄을 받을지도 모르니까요 ^^;;

아마도 여러분은 Global.asax를 사용해 보신 적이 있을 것입니다. 기본적으로 그 파일 안에는 다음과 같은 이벤트들이 있는 것을 아실 것입니다.

Application_OnStart
Session_OnStart
Session_OnEnd
Application_OnEnd

이 이벤트들은 asp 시절에도 있어왔던 것들이기에 이미 다들 이들의 역할을 아실 것이라 생각합니다. 사이트의 카운터를 만들기 위해서, 혹은 방문자 접속 통계를 만들기 위해서 이러한 이벤트들을 사용해 본 경험들이 있으실 것입니다. 그렇죠?

그리고, ASP.NET에서는 Global.asax 파일이 보유하고 있는 이벤트가 ASP 시절보다 더 많이 있다는 것도 아실 것입니다. 당장이라도 Global.asax 파일의 소스를 열어보세요. 아마도 다음과 같은 이벤트들이 있는 것을 보실 수 있을 거예요

Application_BeginRequest
Application_Error

또한, Global.asax 파일의 부모 클래스가 HttpApplication인 것도 보실 수 있을 것입니다. 즉, 이 이야기는 Global.asax도 기본적으로 이러한 HttpApplication 클래스의 이벤트들을 모두 이용할 수 있다는 것이겠죠? 즉, 다음과 같은 이벤트들을 말입니다.

이벤트 설명
BeginRequest 새로운 요청이 들어올 경우 수행된다. 모든 요청에 대해서 반드시 일어난다.
AuthenticationRequest 인증 메커니즘이 해당 요청을 인증한 경우 일어난다
AuthorizationRequest AuthenticationRequest에 이어서, 요청에 허가를 부여한 경우에 발생한다.
ResolveRequestCache 출력 캐시 모듈은 캐싱된 요청 처리를 신속히 처리하기 위해서 이 이벤트를 사용한다.
AcquireRequestState 개별적인 요청 상태를 얻어야 한다는 것을 알린다.
PreRequestHandlerExecute 요청 핸들러가 실행되기 직전에 발생한다. 이 이벤트는 HTTP 핸들러가 호출되기 전에 마지막으로 다룰 수 이는 이벤트이다.
PostRequestHandlerExecute HTTP 핸들러가 요청을 마친 뒤 발생한다.
ReleaseRequestState 어플리케이션이 요청 처리를 마쳤기에, 요청 상태가 저장되어야 한다는 것을 알린다
UpdateRequestCache 코드 처리가 완료되었고, 해당 파일이 ASP.NET 캐시에 추가될 준비가 되었음을 알린다.
EndRequest 요청에 대한 마지막 이벤트. 어플리케이션이 종료될 경우 호출되는 마지막 이벤트이다.

위의 목록은 발생하는 순서에 따른 것입니다. 그리고, 순서없이 특정 상황에 발생하는 이벤트들로는 다음과 같은 것들이 있습니다.

이벤트 설명
Error 처리되지 않은 예외가 발생한 경우 일어난다.
PreSendRequestHeaders 이는 HTTP 헤더가 클라이언트로 내려 보내기 전에 발생한다. 이를 통해 헤더를 추가, 제거, 수정할 수 있다.
PreSendRequestContent 이는 컨텐트를 클라이언트에게 내려 보내기 직전에 발생한다. 이를 통해 컨텐트를 보내기 전에 수정할 수 있다.

이 이벤트들은 정확하게 말하자면, HttpApplication 클래스가 제공하는 이벤트들입니다. 물론, Global.asax 의 경우는 위에 나열한 HttpApplication 클래스의 기본 이벤트 외에도 Session_OnStart와 같은 것들을 추가적으로 구현하고 있긴 합니다만, 기본적으로는 위와 같은 이벤트들을 ASP.NET 런타임으로부터 제공받는 것이죠. 해서, 어플리케이션 공통적으로 위에서 나열한 이벤트들 사이에 어떤 처리를 수행하고 싶을 경우, Globla.asax 파일 안에 직접적으로 필요한 코드를 작성해서 원하는 처리를 구동할 수 있습니다.

하지만, 그렇게 처리해야 할 작업들이 많아지게 되면, Global.asax는 매우 지저분해질 것이고, 업무별로 따로 작성되면 더 좋을 코드들이 하나의 파일안에 모두 뒤죽박죽 섞어질 것이기에, 가독성 측면에서나 차후 관리 및 유지 보수 측면에서도 매우 좋지 않을 것입니다.

또한, 그러한 작업이 현재 제작중인 웹 어플리케이션에서만 사용되어야 하는 것이 아니라, 다른 웹 어플리케이션에서도 필요하다면 어떻게 될까요? 뭐~ 지저분해진 Global.asax 파일을 복사해서 가져다가 편집해서 써도 되겠지만, 그 방법은 그다지 효과적이라고 느껴지지 않을 것입니다. 코드가 변경되거나, 보강되어야 한다면 그렇나 Global.asax들을 다 찾아다니면서 고쳐주어야 할테니까요. 그렇습니다. 생각만해도 끔찍하네요!!

그럴 바에는 차라리, 그러한 기능들을 독립적인 별도의 모듈(혹은 컴포넌트)로 만들어서 재사용하는 쪽으로 설계방향을 잡는 것이 훨씬 더 효과적일 것이라는 생각이 들지 않습니까?

해서, HttpModule이 필요한 것입니다!

Global.asax를 직접적으로 이용하지 않고도, HttpModule을 제작하여 사용하면 바로 그러한 어플리케이션 이벤트들을 런타임 중에 가로챌 수 있으며, 특정 요건에 따라 처리해야 할 작업들을 개별적으로 모듈화 할 수 있습니다. 물론, 그렇기에 재사용이 가능한 것은 두말할 필요도 없겠죠?

예를 들어, 예외가 발생할 경우 그 내역을 파일이나 데이터베이스에 로깅을 하고 싶다면? 그렇다면, 그러한 작업을 Global.asax에 작성하는 것이 아니라 개별적인 HttpModule를 작성해서 사용하면 되는 것입니다. 또한, 웹 페이지의 수행시간이나 요청 정보 통계 같은 것들을 별도로 로깅하고 싶을 경우도 그러한 작업을 Global.asax에 직접 작성하는 것이 아니라 별도의 HttpModule를 작성해서 사용하면 됩니다. 이렇게 되면, 각각의 기능들이 별도로 모듈화 되기에 가독성 측면에서도, 관리, 재 사용성 측면에서도 큰 이익을 볼 수 있을 것입니다 (다만, 성능적으로는 약간의 희생이 요구될 두도 있을 것이긴 합니다).

다소 이야기가 복잡해진 것 같은데, 정리하자면 다음과 같습니다.

HttpModule를 제작하게 되면, 어플리케이션의 여러 이벤트들을 가로채서 어플리케이션 공통적으로 필요한 작업들을 독립적으로 모듈화할 수 있다(해서, 이를 Intercepting Filter라고도 이야기합니다)

위에서 Global.asax와 HttpModule을 비교해 가면서 설명을 하여, 혹시라도 HttpModule이 Global.asax의 대체일 수 있다고 생각하실까 하여 첨언하자면 말입니다. 사실, HttpModule의 역할이 Global.asax의 역할을 대신하기 위한 것은 아닙니다. 이해를 돕기 위해서 Global.asax와 비교해가면서 설명한 것이지, 그것이 주 목적은 아니라는 것을 기억하셔야 합니다. HttpModule은 말 그대로 공통적인 Http 요청에 대응하기 위한 모듈이라는 것을 기억하세염~

참고 : Intercepting Filter 패턴
HttpModule은 웹 어플리케이션 내의 반복적인 기능을 구현하기 위해 Intercepting Filter 패턴을 적용한 기술입니다. 즉, 웹 어플리케이션이 페이지 요청을 처리하기 전이나 후에 고유한 어떤 처리 과정을 추가해야 할 필요가 있을 경우 사용하는 기술입니다. 기존 코드나 프레임워크를 변경함 없이 새로운 모듈을 결합할 수 있는 장점을 제공하는 대신, 성능적인 부담이 있을 수 있는 부분은 항상 유의해야 합니다.

조금 어렵나요? 그렇다면 실제 예제가 필요한 시점이 되겠네요. 일단, 간단한 예제부터 한번 해보고 이야기를 더 진행해보도록 하겠습니다.

예제를 위해서 새로운 ASP.NET 사이트 하나와 클래스 라이브러리를 하나 만들어 보도록 하겠습니다. VS.NET을 열고 우선적으로 ASP.NET 응용 프로그램을 하나 만들어 보도록 하세요. 저는 프로젝트 명칭을 WebSampleApp 라고 주어 보았습니다. 그리고, 그 프로젝트의 [솔루션]에 마우스 우측 클릭을 하고 [추가][새 프로젝트]를 선택해서 AspNetRuntimeEx라는 이름의 클래스 라이브러리 프로젝트를 하나 더 추가했습니다. 그리고, 기본적으로 존재하는 Class1.cs 파일을 제거하고, ExeTimeModule.cs라는 새로운 클래스를 추가하세요. 그렇게 해서 만들어진 VS.NET 프로젝트는 다음과 같습니다.

ExeTimeModule라는 클래스는 HttpModule로서 제작을 할 녀석인데요. 이 친구가 할 작업은 매우 단순합니다. 단지 페이지를 실행되는 데 걸린 시간을 매 페이지의 하단에 출력할 것이거든요. 우선은 단순한 예제로 시작하는 것이 이해하는 데 도움이 되기에 그렇게 하는 것이지, 여러분을 무시하거나 가벼이 여기서 그런 예제를 하는 것은 아님을 알아주십시오 ^^;

그럼 이제 ExeTimeModule 클래스의 구현부로 좋아~ 가는거야~~ (노홍촐 버전입니다) 일단 예제의 전체 소스는 다음과 같습니다.

using System;
using System.Web;

namespace AspNetRuntimeEx
{
    public class ExeTimeModule : IHttpModule
    {
        private DateTime startTime;

        // IhttpModule 인터페이스 구현
        public void Init(HttpApplication application)
        {
            application.BeginRequest += new EventHandler(application_BeginRequest);
            application.EndRequest += new EventHandler(application_EndRequest);
        }

        // IhttpModule 인터페이스 구현
        public void Dispose(){}

        public void application_BeginRequest(object source, EventArgs e)
        {
            startTime = DateTime.Now;
        }

        public void application_EndRequest(object source, EventArgs e)
        {
            //현재의 HttpApplication 개체를 얻어온다
            HttpApplication app = (HttpApplication) source;
            //현재 웹 어플리케이션의 컨텍스트를 얻어온다
            HttpContext context = app.Context;

            TimeSpan span = DateTime.Now - startTime;
            context.Response.Write("<p>페이지 수행시간 : " +
                    span.TotalSeconds.ToString() + " 초</p>");
        }
    }
}

우선, 사용자 정의 HttpModule을 제작하고자 한다면, 반드시 HttpModule은 IHttpModule 인터페이스를 구현해야만 합니다. 그래야만 HttpModule로서 구동을 할 수 있게 됩니다. 해서, 위의 소스에서도 그러한 구현이 있는 것을 볼 수가 있을 것입니다.

참고로, IHttpModule 인터페이스는 다음과 같은 정의를 가지고 있습니다.

public interface IHttpModule
{
    // Methods
    void Init(HttpApplication context);
    void Dispose();
}

그렇기에, 이 인터페이스를 구현하는 클래스는 반드시 이러한 2개의 메서드들을 반드시 정의해야 합니다. 해서, 우리의 예제에서도 그 2개의 메서드를 구현하고 있는 것을 볼 수 있습니다. 소스를 살펴보면, Init 메서드 안에서 어플리케이션 이벤트 중 BeginRequest와 EndRequest에 대한 이벤트 처리기를 걸어두고 있는데요. 이는 요청 시작 시 시작 시간을 저장하기 위해서, 그리고 요청이 끝날 때 시작 시간과의 간격을 구해 그 값을 출력하기 위해서입니다.

application_BeginRequest 이벤트에서는 단지 현재의 시간을 클래스 변수에 넣고 있는 것 뿐이기에 별다른 설명이 필요할 것 같지 않네요. 주목할 부분은 매 페이지의 실행이 끝날 때 수행되는 application_EndRequest 이벤트입니다.

이 이벤트 안에서는 현재의 시간과 시작 시간과의 차이를 구해서 Response.Write로 그 간격 차이를 출력하는 처리를 하는데요. 중요한 부분은 Response 개체를 사용하기 위해서 현재 요청에 대한 Context를 얻어오는 부분입니다.

현재 제작하고 있는 HttpModule은 웹 페이지에 종속적인 것이 아니기에, Response와 같은 개체를 사용해서 현재 실행 중인 페이지에 쓰기 작업을 하려면, 우선적으로 현재 실행중인 페이지의 컨텍스트를 알아야만 합니다.

컨텍스트를 알아내려면 일단 현재 실행 중인 HttpApplication을 알아낼 필요가 있는데요. 이는 이벤트 메서드의 첫 번째 인자를 형 변환하여 얻어낼 수 있습니다. 소스 중 다음 코드가 바로 그것이죠.

//현재의 HttpApplication 개체를 얻어온다
HttpApplication app = (HttpApplication)source;
//현재 웹 어플리케이션의 컨텍스트를 얻어온다
HttpContext context = app.Context;

이 코드는 매우 자주 쓰이므로 기억해 주시거나, 손이 잘 닿을만한 곳에 보관해 두시는 것이 좋습니다. 언제라도 “까삐 앤 뻬이수뚜” 신공을 펼칠 수 있게 말이죠 ^^

컨텍스트를 얻어오게 되면 이 개체를 통해서 기본적인 ASP.NET 페이지의 모든 내장 개체들을 이용할 수가 있습니다. Response는 물론 Request, Server 등등을 말이죠. 해서, 소스에서는 해당 컨텍스트의 Response 개체 속성을 통해서 Write 메서드를 호출하고 있는 것을 볼 수가 있습니다.

그 코드로 인해, 이 모듈은 모든 페이지의 호출 완료 시점에 현재 실행 중인 페이지에 쓰기 작업을 할 수가 있게 되는 것입니다.

이제, 간단하긴 하지만 우리만의 첫 번째 HttpModule를 완성했네요. 그렇다면, 이 모듈을 우리의 웹 어플리케이션에서 사용해야 하겠죠? 사용하는 방법은 매우 간단합니다. 단지, 웹 어플리케이션의 bin 디렉터리에 빌드된 dll을 복사해 넣고, Web.config 파일 안에 가벼운 설정 하나만 해주면 끝이죠. 예전 ISAPI 시절에는 인터넷 서비스 관리자에 가서 필터를 등록해주고 하는 다소 피곤한 작업을 했어야만 하지만, .NET에서는 단지 config 파일에 그러한 설정을 추가해 주기만 하면 끝입니다. 매우 간편해진 것이죠 ^^

그럼 한번 해볼까요?

우선 빌드된 dll을 우리의 웹 어플리케이션의 bin 디렉터리로 복사해올 필요가 있을텐데요. 이것을 수동으로 하기에는 약간의 귀찮음이 있으니 간편하게 하기 위해서, 웹 어플리케이션에서 AspNetRuntimeEx 프로젝트를 [프로젝트 참조] 하도록 하세요. 이를 위해서는 웹 어플리케이션 프로젝트에서 마우스 우측 클릭하고, [참조 추가] 하신 다음, 다음 그림과 같이 프로젝트 참조를 잡아주기만 하면 됩니다.

이렇게 하고 웹 어플리케이션을 빌드하면 자동으로 참조된 프로젝트의 dll 들이 웹 어플리케이션의 bin 디렉터리로 자동 복사가 되니까요.

OK! 자. 이제 어셈블리 배포도 일단은 완료되었네요. 그럼 남아있는 것은 web.config 파일에 HttpModule를 장착하는 것만이 남았습니다. Web.config 파일을 열어서 다음과 같이 추가를 해 주세요. ^^

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.web>
        <httpModules>
            <add name="ExeTimeWriter" type="AspNetRuntimeEx.ExeTimeModule, AspNetRuntimeEx" />
        </httpModules>
        <!-- 동적 디버깅 컴파일
            compilation debug="true"로 설정하여 컴파일된 페이지에
            디버깅 기호(.pdb 정보)를 삽입합니다. 이렇게 하면 파일 크기가 커져서 실행 속도가
            느려지므로 디버깅 하는 경우에만 이 값을 true로 설정하고
            다른 모든 경우에는 false로 설정해야 합니다. 자세한 내용은
            ASP.NET 파일 디버깅에 대한 설명서를 참조하십시오.
        -->
        <compilation defaultLanguage="vb" debug="true" />

<system.web> 섹션의 하위로 <httpModules>를 추가해 준 다음, 여러분이 제작한 HttpModule들을 Add 해 주기만 하면 됩니다. 제작한 다른 모듈들이 있다면 그들도 원하는 만큼 추가할 수 있겠죠? Add할 경우, name은 그냥 고유한 이름을 주시면 됩니다. 중요한 것은 type인데요. Type는 다음과 같은 형식으로 지정해 주셔야 합니다.

Type="네임스페이스.클래스명, 어셈블리명(.dll을 뺀 명칭)"

좋습니다. 여기까지 지정했다면 이제 우리만의 필터인 HttpModule은 등록이 된 것입니다.

이제 잘 동작하는지를 테스트해 보기 위해서, aspx 페이지를 하나 만들어 보도록 하겠습니다. Aspx 페이지 자체에 특별한 기능이 존재하는 것은 아니구요. 단지, 그 페이지에는 실행을 약간 더디게 만드는 로직만이 들어있을 뿐입니다. 즉, Thread.Sleep를 써서 약 2초 정도 실행을 중지하려 하는 것인데요. 그런 로직을 사용하지 않으면, 실행시간이 너무 빨라서 아마도 HttpModule이 산출해내는 실행 시간이 언제나 0이 나올 가능성이 있거든요 ^^

자. 여러분도 다음과 같은 Default.aspx 페이지를 하나 만들도록 하세요.

<%@ Page Language="vb" AutoEventWireup="false" Codebehind="Default.aspx.vb"
Inherits="WebSampleApp._Default"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
    <HEAD>
        <title>Default</title>
        <style>p {font-family:verdana; font-size:11px}</style>
    </HEAD>
    <body>
        <form id="Form1" method="post" runat="server">
            <p>저는 Default.aspx 페이지입니다.</p>
        </form>
    </body>
</HTML>

그리고, 코드 비하인드 페이지의 Page_Load 이벤트 코드는 다음과 같습니다.

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    System.Threading.Thread.Sleep(2000)
End Sub

다 되었으면, 이제 실행을 해 봅니다. 그러면, 페이지가 실행될 때마다, HttpModule이 구동하여 페이지의 구동이 끝난 후, 페이지의 하단에 실행 시간을 출력해 주는 것을 보실 수 있을 것입니다. 다음과 같이 말이죠 ^^

+ Recent posts