Extreme ASP.NET

ASP.NET Web Forms를 사용한 라우팅

Scott Allen

코드는 MSDN 코드 갤러리에서 다운로드할 수 있습니다.
온라인으로 코드 찾아보기

목차

라우팅이란?
URL 재작성의 간략한 역사
경로 및 경로 처리기
ASP.NET의 라우팅 구성
경로 구성
작성법 라우팅 처리기
라우팅과 보안
URL 생성
요약

Microsoft .NET Framework 3.5 서비스 팩 1에서는 ASP.NET 런타임에 라우팅 엔진이 새로 추가되었습니다. 이 라우팅 엔진은 들어오는 HTTP 요청의 URL을 요청에 응답하는 실제 Web Form과 분리하여 웹 응용 프로그램에 사용할 알아보기 쉬운 URL을 작성할 수 있도록 해줍니다. 이전 버전의 ASP.NET에서도 알아보기 쉬운 URL을 사용할 수는 있지만 라우팅 엔진은 보다 쉽고 깔끔하며 테스트하기 용이한 사용 방식을 제공합니다.

원래 라우팅 엔진은 현재 사전 검토 단계에 있는 ASP.NET MVC(Model View Controller) 프레임워크의 일부로 개발되기 시작했습니다. 그러나 Microsoft는 System.Web.Routing 어셈블리에 라우팅 논리를 추가하고 SP1에 이 어셈블리를 포함하여 발표했습니다. 현재 이 어셈블리는 마찬가지로 SP1에 포함된 ASP.NET Dynamic Data 기능에 대해 라우팅 기능을 제공하고 있지만 이 칼럼에서는 ASP.NET Web Forms에 라우팅 기능을 사용하는 방법을 설명하고자 합니다.

라우팅이란?

RecipeDisplay.aspx라는 ASP.NET Web Form이 Web Forms라는 폴더에 저장되어 있다고 가정해 보겠습니다. 이러한 Web Form의 작성법을 보기 위한 전형적인 방법은 폼의 실제 위치를 가리키는 URL을 작성하고 쿼리 문자열에 몇 가지 데이터를 인코딩하여 Web Form에 표시할 작성법을 알려 주는 것입니다. 이러한 URL은 /WebForms/RecipeDisplay.aspx?id=5와 같은 형식으로 끝납니다. 여기에서 숫자 5는 작성법이 저장된 데이터베이스 테이블의 기본 키 값을 나타냅니다.

기본적으로 라우팅은 URL 끝점을 여러 매개 변수로 분할한 다음 해당 매개 변수를 사용하여 특정 구성 요소로 HTTP 요청이 처리되도록 지시하는 프로세스라고 할 수 있습니다. URL /recipe/5를 예로 들어보겠습니다. 라우팅 구성이 올바르다면 여전히 Web Form RecipeDisplay.aspx를 사용하여 URL에 응답할 수 있습니다.

URL은 더 이상 실제 경로를 나타내지 않습니다. 대신 recipe라는 단어가 라우팅 엔진이 작성법 요청 처리를 위한 구성 요소를 찾는 데 사용할 수 있는 매개 변수를 나타냅니다. 숫자 5는 요청을 처리하는 동안 특정 작성법을 표시하는 데 필요한 두 번째 매개 변수를 나타냅니다. 데이터베이스 키를 URL로 인코딩하는 대신 /recipe/tacos와 같은 URL을 사용하는 편이 효율적입니다. 이 URL은 특정 작성법을 표시하기에 충분할 뿐만 아니라 사용자가 알아보기에도 편한 매개 변수를 제공하고 최종 사용자가 요청 의도를 알 수 있도록 하며 검색 엔진에서 검색해야 할 중요한 키워드를 포함합니다.

URL 재작성의 간략한 역사

기존에는 ASP.NET에서 /recipe/tacos와 같은 형식으로 끝나는 URL을 사용하려면 URL 재작성 방식을 사용해야 했습니다. URL 재작성에 대한 자세한 내용은 Scott Mitchell의 기사 "ASP.NET에서 URL 재작성"을 참조하십시오. 이 기사는 ASP.NET에서 HTTP 모듈과 HttpContext 클래스의 정적 RewritePath 메서드를 사용하여 URL 재작성을 구현하는 일반적인 방법을 설명합니다. 또한 알아보기 쉽고 분석 가능한 URL의 이점에 대해서도 자세히 설명하고 있습니다.

이전에 RewritePath API를 사용해본 독자라면 재작성 방식의 몇 가지 문제와 단점을 잘 알고 있을 것입니다. RewritePath에서 가장 문제가 되는 부분은 메서드가 요청을 처리하는 동안 사용되는 가상 경로를 변경하는 방식입니다. URL 재작성의 경우 재작성된 내부 URL로의 포스트백을 방지하려면 각 Web Form의 포스트백 대상을 수정해야 합니다. 이를 위해서 요청 시에 URL을 다시 한 번 재작성하는 경우가 많습니다.

또한 URL 재작성 논리가 양방향으로 작동하도록 하는 손쉬운 메커니즘이 없기 때문에 대부분은 개발자는 URL 재작성을 단방향 변환으로 구현합니다. 예를 들어 URL 재작성 논리에 공용 영역에 접한 URL을 제공하고 논리에서 Web Form의 URL이 반환되도록 하기는 쉽지만 재작성 논리에 Web Form의 내부 URL을 제공하여 폼에 연결하는 데 필요한 공용 URL을 반환하도록 하기는 어렵습니다. 후자의 경우 재작성된 URL에 숨겨진 다른 Web Form에 대한 하이퍼링크를 생성하는 데 유용합니다. 이 칼럼의 나머지 부분에서 살펴보겠지만 URL 라우팅 엔진은 이러한 문제를 해결해 줍니다.

fig01.gif

그림 1 경로, 경로 처리기 및 라우팅 모듈

경로 및 경로 처리기

경로, 경로 처리기 및 라우팅 모듈은 URL 라우팅 엔진의 세 가지 기본 요소입니다. 경로는 URL을 경로 처리기에 연결합니다. System.Web.Routing 네임스페이스의 Route 클래스 인스턴스는 런타임에 경로를 나타내며 경로의 매개 변수와 상수를 기술합니다. 경로 처리기는 System.Web.Routing.IRouteHandler 인터페이스에서 상속됩니다. 이 인터페이스는 경로 처리기가 GetHttpHandler 메서드를 구현하도록 합니다. GetHttpHandler 메서드는 IHttpHandler 인터페이스를 구현하는 개체를 반환합니다. IHttpHandler 인터페이스는 ASP.NET이 처음 개발될 당시부터 ASP.NET에 포함되어 있었으며 Web Form(System.Web.UI.Page)도 하나의 IHttpHandler입니다. Web Form에 라우팅을 사용하는 경우 경로 처리기가 올바른 Web Form을 찾고 인스턴스화하고 반환해야 합니다. 마지막으로 라우팅 모듈은 ASP.NET 처리 파이프라인에 연결됩니다. 이 모듈은 들어오는 요청을 가로채 URL을 확인하여 정의된 경로 중에 일치하는 경로를 찾습니다. 이때 라우팅 모듈은 연결된 경로 처리기에서 일치하는 경로를 검색하고 라우팅 처리기에 요청을 처리할 IHttpHandler를 묻습니다.

지금까지 설명한 세 가지 기본 형식은 그림 1에 나와 있습니다. 다음 섹션에서는 이 세 가지 요소의 작동 원리를 살펴보겠습니다.

ASP.NET의 라우팅 구성

ASP.NET 웹 사이트 또는 웹 응용 프로그래밍의 라우팅을 구성하려면 먼저 System.Web.Routing 어셈블리에 대한 참조를 추가해야 합니다. .NET Framework 3.5의 SP1을 설치하면 전역 어셈블리 캐시에 이 어셈블리가 설치되고 표준 "참조 추가" 대화 상자에서 이 어셈블리를 찾을 수 있습니다.

다음으로 ASP.NET 파이프라인에 라우팅 모듈도 구성해야 합니다. 라우팅 모듈은 표준 HTTP 모듈입니다. IIS 6.0 이전 버전과 Visual Studio 웹 개발 서버의 경우 다음과 같이 web.config의 <httpModules> 섹션을 사용하여 라우팅 모듈을 설치합니다.

<httpModules>      
    <add name="RoutingModule" 
         type="System.Web.Routing.UrlRoutingModule, 
               System.Web.Routing, 
               Version=3.5.0.0, Culture=neutral, 
               PublicKeyToken=31bf3856ad364e35"/>

    <!-- ... -->

</httpModules>

IIS 7.0에서 라우팅 기능을 사용하여 웹 사이트를 실행하려면 web.config에 두 가지 항목이 필요합니다. 그중 첫 번째 항목은 <system.webServer>의 <modules> 섹션에 있는 URL 라우팅 모듈 구성이며 두 번째는 <system.webServer>의 <handlers> 섹션에 있는 UrlRouting.axd에 대한 요청 처리 항목입니다. 이 두 항목은 그림 2에 나와 있습니다. "IIS 7.0 구성 항목" 보충 기사도 참조하십시오.

그림 2 URL 라우팅 모듈 구성

<system.webServer>
   <modules runAllManagedModulesForAllRequests="true">

      <add name="UrlRoutingModule"
             type="System.Web.Routing.UrlRoutingModule, 
                   System.Web.Routing, Version=3.5.0.0, 
                   Culture=neutral, 
                   PublicKeyToken=31BF3856AD364E35" />
      <!-- ... -->

    </modules>
    <handlers>

      <add name="UrlRoutingHandler"
            preCondition="integratedMode"
            verb="*" path="UrlRouting.axd"
            type="System.Web.HttpForbiddenHandler, 
                  System.Web, Version=2.0.0.0, Culture=neutral, 
                  PublicKeyToken=b03f5f7f11d50a3a" />
      <!-- ... -->

    </handlers>
</system.webServer>

파이프라인에 URL 라우팅 모듈을 구성한 후에는 모듈을 PostResolveRequestCache 이벤트와 PostMapRequestHandler 이벤트에 연결합니다. 그림 3은 이 두 가지 파이프라인 이벤트의 일부를 보여 줍니다. 일반적으로 URL 재작성 구현은 BeginRequest 이벤트 동안 작업을 수행합니다. 이 이벤트는 요청을 처리하는 동안 가장 먼저 발생하는 이벤트입니다. URL 라우팅을 사용하는 경우 경로 처리기의 경로 검색 및 선택은 PostResolveRequestCache 단계에서 이루어집니다. 이 단계는 처리 프로세스에서 인증, 권한 부여 및 캐시 조회 단계가 완료된 후에 실행됩니다. 이러한 이벤트 타이밍이 갖는 의미에 대해서는 뒷부분에서 다시 설명하겠습니다.

fig03.gif

그림 3 HTTP 요청

경로 구성

경로와 경로 처리기는 서로 유기적으로 관련되어 있지만 경로를 구성하는 코드를 먼저 살펴보도록 하겠습니다. 라우팅 엔진의 RouteTable 클래스는 정적 Routes 속성을 통해 RouteCollection을 제공합니다. 모든 사용자 지정 경로는 응용 프로그램에서 첫 번째 요청이 실행되기 전에 이 컬렉션에 구성해야 합니다. 이는 곧 global.asax 파일과 Application_Start 이벤트를 사용해야 함을 의미합니다.

그림 4는 RecipeDisplay.aspx Web Form에 연결하기 위해 "/recipe/brownies"에 대해 사용해야 하는 경로 등록 코드를 보여 줍니다. RouteCollection 클래스에 대한 Add 메서드의 매개 변수에는 경로 이름과 경로 자체가 차례로 포함되어 있습니다. Route 생성자의 첫 번째 매개 변수는 URL 패턴입니다. 이 패턴은 해당 응용 프로그램을 가리키는 URL의 끝에 표시할 URL 세그먼트로 구성됩니다. (응용 프로그램의 루트에 연결하는 데 필요한 모든 세그먼트가 이 세그먼트의 앞에 오게 됩니다.) 응용 프로그램의 루트가 localhost/food/인 경우 그림 4의 경로 패턴은 localhost/food/recipe/brownies에 해당합니다.

그림 4 /recipe/brownies의 경로 등록 코드

protected void Application_Start(object sender, EventArgs e)
{
    RegisterRoutes();
}

private static void RegisterRoutes()
{
    RouteTable.Routes.Add(
        "Recipe",
        new Route("recipe/{name}", 
                  new RecipeRouteHandler(
                      "~/WebForms/RecipeDisplay.aspx")));
}

IIS 7.0 구성 항목

이 샘플에서처럼 확장명 없는 URL을 사용하려면 runAllManagedModulesForAllRequests 특성의 값이 true로 설정되어야 합니다. 또한 UrlRouting.axd에 대해 HTTP 처리기를 구성하는 것이 이상하게 생각될 수도 있습니다. 이는 IIS 7.0에서 라우팅이 제대로 작동하도록 하기 위해 라우팅 엔진에 요구되는 구성입니다. 실제로 UrlRouting 모듈은 들어오는 URL을 ~/UrlRouting.axd로 재작성하고 그에 따라 URL이 다시 원래 들어오는 URL로 재작성됩니다. IIS의 새 버전에는 라우팅 엔진이 완벽하게 통합되어 이러한 구성이 필요 없게 되리라 예상됩니다.

중괄호 안에 있는 세그먼트는 매개 변수를 나타내며 라우팅 엔진은 이러한 매개 변수에서 자동으로 값을 추출하여 요청을 처리하는 동안 유지되는 이름/값 사전에 저장합니다. 앞서 살펴본 localhost/food/recipe/brownies 예제를 사용할 경우 라우팅 엔진은 "brownies"라는 값을 추출하여 "name"이라는 키와 함께 사전에 저장합니다. 이 사전을 사용하는 방법은 경로 처리 코드를 살펴볼 때 설명하도록 하겠습니다.

RouteTable에 추가하는 경로의 수에는 제한이 없지만 경로의 순서는 매우 중요합니다. 라우팅 엔진은 컬렉션에 있는 순서대로 경로에 대해 모든 들어오는 URL을 테스트하여 패턴이 일치하는 첫 번째 경로를 선택합니다. 때문에 가장 구체적인 경로를 먼저 추가해야 합니다. URL 패턴이 "{범주}/{하위 범주}"의 형식인 포괄적인 경로를 작성법 경로보다 먼저 추가하면 라우팅 엔진이 작성법 경로를 찾지 못합니다. 한 가지 덧붙이자면 라우팅 엔진은 패턴을 검색할 때 대/소문자를 구분합니다.

Route 생성자의 오버로드된 버전을 사용하면 기본 매개 변수 값을 만들고 제약 조건을 적용할 수 있습니다. 이러한 기본값을 사용하면 들어오는 URL의 매개 변수에 해당하는 값이 없을 때 라우팅 엔진이 이름/값 매개 변수 사전에 추가할 기본값을 지정할 수 있습니다. 예를 들어 localhost/food/recipe처럼 이름 값이 없는 작성법 URL을 수신했을 때 라우팅 엔진이 사용할 기본 작성법 이름으로 "brownies"를 지정할 수 있습니다.

제약 조건을 사용하면 매개 변수의 유효성을 검사하는 정규식을 지정하고 들어오는 URL에 대한 경로 패턴 검색을 미세 조정할 수 있습니다. URL에 기본 키 값을 사용하여 작성법을 식별하는 경우(예: localhost/food/recipe/5) 정규식을 사용하여 URL의 기본 키 값이 정수인지 확인할 수 있습니다. 또한 IRouteConstraint 인터페이스를 구현하는 개체를 사용하여 제약 조건을 적용할 수도 있습니다.

Route 생성자의 두 번째 매개 변수는 그림 5에서 살펴볼 경로 처리기의 새 인스턴스입니다.

그림 5 RecipeRouteHandler

public class RecipeRouteHandler : IRouteHandler
 {
     public RecipeRouteHandler(string virtualPath)
     {
         _virtualPath = virtualPath;
     }

     public IHttpHandler GetHttpHandler(RequestContext requestContext)
     {
         var display = BuildManager.CreateInstanceFromVirtualPath(
                         _virtualPath, typeof(Page)) as IRecipeDisplay;

         display.RecipeName = requestContext.RouteData.Values["name"] as string;
         return display;
     }

     string _virtualPath;
 }

작성법 라우팅 처리기

다음 코드 조각은 작성법 요청에 대한 경로 처리기의 기본 구현을 보여 줍니다. 경로 처리기는 궁극적으로 IHttpHandler(예제의 경우 RecipeDisplay.aspx)를 만들어야 하므로 경로 처리기가 만들 Web Form을 가리키는 가상 경로가 생성자에 필요합니다. GetHttpHandler 메서드는 인스턴스화된 Web Form을 검색하기 위해 이 가상 경로를 ASP.NET BuildManager에 전달합니다.

interface IRecipeDisplay : IHttpHandler
{
    string RecipeName { get;  set; }
}

경로 처리기가 라우팅 엔진의 매개 변수 사전에서 데이터를 가져올 수도 있음을 알 수 있습니다. 여기서는 RequestContext 클래스의 RouteData 속성을 가져왔습니다. 라우팅 엔진은 RequestContext를 설정하고 이 메서드를 호출할 때 인스턴스를 전달합니다. 경로 데이터는 다양한 방법으로 Web Form으로 가져올 수 있습니다. 예를 들어 HttpContext Items 컬렉션에 포함하여 경로 데이터를 전달할 수도 있습니다. 이 예제에서는 구현할 Web Form에 대한 인터페이스(IRecipeDisplay)를 정의했습니다. 경로 처리기는 Web Form에 대해 강력한 형식의 속성을 설정하여 Web Form에 필요한 정보를 전달할 수 있습니다. 이 방법은 ASP.NET 웹 사이트 컴파일 모델과 ASP.NET 응용 프로그램 컴파일 모델 모두에 사용할 수 있습니다.

라우팅과 보안

ASP.NET 라우팅을 사용하는 경우에도 마스터 페이지, 출력 캐싱, 사용자 정의 컨트롤 등 유용한 ASP.NET 기능을 모두 사용할 수 있습니다. 단, 한 가지 주의해야 할 예외가 있습니다. 라우팅 모듈은 처리 프로세스의 인증 및 권한 부여 단계 후에 발생하는 파이프라인 이벤트를 사용하여 작업을 처리하기 때문에 ASP.NET에서는 요청을 처리하기 위해 경로 처리기가 선택한 ASP.NET Web Form에 대한 가상 경로가 아니라 표시되는 공용 URL을 사용하여 사용자에게 권한을 부여하게 됩니다. 따라서 라우팅을 사용하는 응용 프로그램의 권한 부여 전략을 신중하게 계획해야 합니다.

인증된 사용자만 작성법을 볼 수 있도록 하려는 경우를 예로 들어보겠습니다. 이 경우 다음과 같은 권한 부여 설정을 사용하도록 루트 web.config를 수정하는 것이 한 가지 방법이 될 수 있습니다.

<location path="recipe">
  <system.web>
    <authorization>
      <deny users="?"/>
    </authorization>
  </system.web>
</location>

이 방법을 사용하면 익명 사용자가 /recipe/tacos를 볼 수는 없지만 근본적인 취약점이 두 가지 있습니다. 첫째, 이 설정으로는 사용자가 /WebForms/RecipeDisplay.aspx를 직접 요청하지 못하도록 할 수 없습니다. (모든 사용자가 Web Forms 폴더에서 리소스를 직접 요청하지 못하도록 하는 권한 부여 규칙을 추가하여 이 문제를 해결할 수는 있습니다.) 둘째, 권한 부여 규칙을 변경하지 않더라도 global.asax.cs의 경로 구성을 쉽게 변경할 수 있으므로 중요한 작성법이 익명 사용자에게 노출됩니다.

이러한 권한 부여 방식의 대안으로 실제 위치에 따라 RecipeDisplay.aspx Web Form을 보호할 수 있습니다. 즉, <authorization> 설정이 있는 web.config 파일을 보호되는 폴더에 직접 넣는 것입니다. 그러나 ASP.NET에서는 공용 URL을 기준으로 사용자에게 권한을 부여하기 때문에 경로 처리기가 사용하는 가상 경로에 대한 권한 부여 검사를 수동으로 구성해야 합니다.

경로 처리기의 GetHttpHandler 메서드 시작 부분에 다음 코드를 추가해야 합니다. 이 코드는 UrlAuthorizationModule 클래스(ASP.NET 파이프라인에서 권한 부여 검사를 수행하는 모듈과 같은 모듈)의 정적 CheckUrlAccessForPrincipal 메서드를 사용합니다.

if (!UrlAuthorizationModule.CheckUrlAccessForPrincipal(
    _virtualPath, requestContext.HttpContext.User,
                  requestContext.HttpContext.Request.HttpMethod))
{
    requestContext.HttpContext.Response.StatusCode = 
        (int)HttpStatusCode.Unauthorized;
    requestContext.HttpContext.Response.End(); 
}

RequestContext를 통해 HttpContext 멤버에 액세스하려면 System.Web.Abstractions 어셈블리에 대한 참조를 추가해야 합니다.

안전한 라우팅 처리기가 구성되었으므로 이제 데이터베이스의 각 작성법에 대한 하이퍼링크를 생성하는 페이지를 작성할 차례입니다. 라우팅 논리는 이 페이지를 작성하는 데에도 도움이 됩니다.

URL 생성

특정 작성법에 대한 하이퍼링크를 생성하기 위해 응용 프로그램 시작 시에 구성된 경로 컬렉션을 다시 한번 살펴보도록 하겠습니다. 다음에서 보듯이 RouteCollection 클래스에는 이 용도로 사용되는 GetVirtualPath 메서드가 있습니다.

VirtualPathData pathData = 
    RouteTable.Routes.GetVirtualPath(
                  null,
                  "Recipe",
                  new RouteValueDictionary { { "Name", recipeName } });

return pathData.VirtualPath;

원하는 경로 이름("Recipe")을 필요한 매개 변수 및 관련 값의 사전과 함께 전달해야 합니다. 이 메서드는 앞서 만든 URL 패턴(/recipe/{name})을 사용하여 올바른 URL을 생성합니다.

다음 코드에서는 이 메서드를 사용하여 익명 형식 개체의 컬렉션을 생성합니다. 이러한 개체에는 데이터 바인딩을 통해 사용 가능한 작성법의 목록 또는 테이블을 생성하는 데 사용할 수 있는 Name 및 Url 속성이 있습니다.

var recipes = 
    new RecipeRepository()
        .GetAllRecipeNames()
        .OrderBy(recipeName => recipeName)
        .Select(recipeName =>
          new
          {
            Name = recipeName,
            Url = GetVirtualPathForRecipe(recipeName)
          });

라우팅 구성에서 URL을 생성할 수 있다는 것은 응용 프로그램 내에 끊어진 링크를 만들 우려 없이 구성을 자유롭게 변경할 수 있음을 의미합니다. 물론 사용자의 즐겨찾기 링크와 책갈피가 손상될 수도 있지만 응용 프로그램의 URL 구조를 설계하는 동안에는 변경이 가능하다는 사실이 큰 이점이 됩니다.

요약

URL 라우팅 엔진은 URL 패턴 검색 및 URL 생성과 관련한 모든 번거로운 작업을 대신 해줍니다. 개발자는 경로를 구성하고 경로 처리기를 구현하기만 하면 됩니다. 라우팅 기능을 사용하면 파일 확장명이나 파일 시스템의 실제 레이아웃에 대해 고민할 필요가 없고 URL 재작성기를 사용하는 번거로움이 해소됩니다. 따라서 최종 사용자와 검색 엔진을 위한 최적의 URL 설계를 개발하는 데 집중할 수 있습니다. 나아가 Microsoft는 향후 발표될 ASP.NET 4.0에서 Web Forms의 URL 라우팅을 보다 쉽고 구성이 용이하도록 만들 예정입니다.

질문이나 의견이 있으면 xtrmasp@microsoft.com으로 보내시기 바랍니다.

Scott Allen은 OdeToCode의 설립자이자 Pluralsight 기술 담당자입니다. 문의 사항이 있으면 scott@odetocode.com 또는 블로그 OdeToCode.com/blogs/scott으로 연락하십시오.