Web Migration

WebMatrix에서 ASP.NET MVC 3으로 웹 응용 프로그램 이동

Brandon Satrom

코드 샘플 다운로드

지난 1월 Microsoft는 Microsoft .NET Framework에서 사용할 수 있는 새로운 웹 개발 프로그래밍 모델인 ASP.NET Web Pages를 소개했습니다. Web Pages는 현재 WebMatrix(web.ms/WebMatrix)에서 기본적으로 지원되는 PHP와 매우 비슷한 페이지 중심의 프로그래밍 모델이며, 각 페이지가 브라우저에 HTML을 렌더링하기 위한 자체 비즈니스 논리, 데이터 액세스 및 동적 컨텐츠를 포함하고 있습니다.

WebMatrix를 사용해서 웹 사이트를 구축하는 데는 여러 가지 이유가 있습니다. 그러나 나중에 특정 시점에 Visual Studio로 이동해야 한다면 어떻게 해야 할까요? ASP.NET MVC 3이 최종적인 상태라면 마이그레이션해야 할 때 사이트를 다시 개발해야 할까요? 운신의 폭이 WebMatrix와 Web Pages로 제한되는 것이 신경 쓰인다면 걱정하지 마십시오.

Web Pages는 핵심 ASP.NET Framework의 일부이며 유연성을 염두에 두고 만들어졌습니다. Web Pages에서 ASP.NET MVC로 이동해야 하는 기술적인 제한은 없지만 여러분의 팀이나 제품 또는 회사의 특별한 이유 때문에 이러한 이동이 필요한 상황이 있을 수 있습니다.

이 기사에서는 마이그레이션을 선택하는 몇 가지 이유(그리고 선택하지 않는 몇 가지 이유)에 대해 이야기해 보겠습니다. 이동을 선택하는 경우를 위해 Web Pages 사이트를 ASP.NET MVC로 이동하는 데 대한 전략도 알아보겠습니다. 페이지에서 뷰로 이동하는 방법, 비즈니스 논리와 도우미 코드를 처리하는 방법, 그리고 모델을 응용 프로그램에 도입하는 방법도 알아볼 것입니다. 마지막으로 라우팅을 통해 기존 사이트 URL을 보존하는 방법과 필요한 경우 영구 리디렉션을 지원하는 방법도 설명하겠습니다.

마이그레이션 시기

Web Pages에서 ASP.NET MVC로 마이그레이션하기를 원하는 이유를 설명하기 전에 원하지 않는 몇 가지 이유부터 알아보겠습니다. 우선 응용 프로그램의 확장성 때문에 사이트를 Web Pages에서 ASP.NET MVC로 이동할 필요는 없습니다. Web Pages는 ASP.NET에 기반을 두고 있으므로 ASP.NET MVC 또는 Web Forms용으로 작성된 응용 프로그램과 동일한 성능 특성을 많이 공유합니다. 각각의 응용 프로그램 유형이 미세하게 다른 실행 모델을 사용하는 것은 사실이지만 근본적으로 ASP.NET MVC가 Web Pages 응용 프로그램에 비해 확장성이 높거나 낮을 이유는 없습니다. 확장성과 성능은 개발자가 선택하는 기반 프레임워크도 중요하지만 사이트를 구축하는 동안 내리는 디자인 결정도 중요합니다.

멋있고 좋아 보인다거나 다른 사람들이 이미 그렇게 하고 있다고 해서 Web Pages에서 ASP.NET MVC로 이동하는 것도 좋은 생각이 아니며, Visual Studio를 사용해서 사이트를 구축하기 위해서라는 것도 좋은 이유가 아닙니다. Web Pages 사이트에서도 이미 가능하기 때문입니다. ASP.NET MVC는 웹 응용 프로그램 아키텍처의 한 가지 선택일 뿐이며 즉시 사이트를 좋게 만들 수 있는 마법 지팡이가 아닙니다. Web Pages에서 ASP.NET MVC로의 마이그레이션은 하나의 선택이며 필수는 아닙니다. 그리고 Microsoft에서 이 마이그레이션을 비교적 힘들게 않게 가능하게 하는 훌륭한 성과를 거두었지만 여전히 다른 마이그레이션과 마찬가지로 시간과 자원, 그리고 비용이 필요합니다. 따라서 ASP.NET MVC로 마이그레이션해야 하는 분명한 이유가 있어야 합니다.

올바른 이유 중 하나는 단위 테스트가 여러분과 여러분의 팀에 중요한 경우입니다. Web Pages 모델은 페이지 중심이므로 Web Pages 사이트에 대해서는 기존 단위 테스트 도구를 사용할 수 없습니다. 웹 UI 테스트의 경우에는 WatiN 또는 Selenium을 사용하는 것이 가능하지만 NUnit 또는 MsTest와 같은 코드 수준 단위 테스트 도구는 사용할 수 없습니다. 사이트가 복잡해지고 단위 테스트가 중요하다면 ASP.NET MVC로 마이그레이션하는 것이 유리합니다.

코드를 단위 테스트하기를 선호한다면 응용 프로그램의 주요 영역을 분리하는 조치를 취했을 가능성이 있습니다. Web Pages에서도 도우미와 코드 파일을 사용하면 깔끔하고 분리된 코드를 만들 수 있기는 하지만 모델 자체에서는 ASP.NET MVC와 같은 자연스러운 분리가 제공되지 않습니다. 주요 영역의 분리가 중요하고 이러한 분리가 원활한 응용 프로그램 아키텍처를 원한다면 마이그레이션이 올바른 선택입니다.

이 두 가지가 아니면 마이그레이션을 수행하는 다른 이유는 사이트나 조직의 상황에 따른 것입니다. 팀이 성장하고 있고 사이트가 점차 복잡해지고 있으며 풍부한 비즈니스 기능이 필요하다면 마이그레이션하는 것이 현명합니다. 소스 제어나 부하 테스트와 같은 풍부한 개발 환경을 더 잘 이용하려는 경우에도 마이그레이션이 필수적일 수 있습니다.

마이그레이션 준비

이 기사에서는 WebMatrix에서 제공되는 사진 갤러리 템플릿 사이트를 ASP.NET MVC로 마이그레이션해 보겠습니다. 마이그레이션 프로세스의 핵심은 페이지에서 모델, 컨트롤러 및 뷰로 이동하는 것이지만 먼저 약간의 준비 작업이 필요합니다.

이 기사에서는 수동 마이그레이션의 단계를 자세하게 살펴보려고 하므로 각 단계를 다른 비중으로 설명하겠습니다. 이 기사의 목표는 중요한 단계를 해결하고 사소한 고려 사항들도 언급하는 것입니다. 이 밖에 전반적인 주제의 핵심과 관련이 없는 데이터 액세스 모범 사례, 잠재적인 어셈블리 구조, 종속성 주입과 같은 내용에 대한 설명은 생략했습니다. 이러한 항목도 중요하지만 대부분은 개발 환경이나 개인적인 선호에 따라 결정되며, 마이그레이션 후 리팩터링 단계에서 다룰 수 있습니다.

그리고 이 기사에서는 현재 사이트를 Visual Studio에서 웹 사이트 프로젝트로 여는 WebMatrix의 “Visual Studio에서 열기 기능”을 사용하지 않았습니다. 물론 이 기능은 ASP.NET MVC로 마이그레이션하지 않더라도 자유롭게 사용할 수 있지만, 필자는 ASP.NET MVC용 Visual Studio 웹 응용 프로그램 프로젝트 유형을 사용해서 빈 사이트로 시작하여 수동으로 항목을 마이그레이션하는 방법을 선호합니다.

파일 | 새로 만들기 | 프로젝트를 선택하고, ASP.NET MVC 3 웹 응용 프로그램을 빈 템플릿으로 선택하며, 기본 보기 엔진으로는 Razor를 사용합니다.

대상 응용 프로그램을 설정한 후에는 약간의 초기 마이그레이션 작업을 수행해야 합니다. 초기 단계를 정리하면 다음과 같습니다.

  1. Web Pages 사이트에서 사용 중인 모든 패키지를 NuGet(nuget.org)을 통해 ASP.NET MVC 사이트로 추가합니다.
  2. System.Web.Helpers, WebMatrix.Data 및 WebMatrix.WebData에 대한 참조를 추가합니다. 속성 창에서 각 항목의 로컬 복사를 true를 설정합니다.
  3. _AppStart.cshtml의 내용을 Global.asax의 Application_Start 메서드로 이동합니다. _AppStart를 그대로 이동하고 사용하는 것도 가능하지만 그보다는 포함된 논리를 ASP.NET MVC 시작 코드와 함께 Global.asax로 집중하는 것이 좋습니다.
  4. <roleManager enabled=true />를 web.config의 <system.web> 섹션으로 추가합니다. 사진 갤러리 응용 프로그램은 WebMatrix.WebData에 있는 새로운 WebSecurity 멤버 자격 공급자를 사용하므로 구성에 해당 항목이 있어야 사이트가 작동합니다.
  5. 응용 프로그램의 Content 또는 Scripts 폴더에 있는 스타일시트, 스크립트 파일 및 이미지 파일을 모두 이동합니다. 이러한 파일에 있는 리소스 링크를 모두 해당하는 새 경로로 업데이트합니다.
  6. Global.asax의 기본 Route를 수정하여 Gallery 컨트롤러와 Default 작업을 가리키도록 합니다.
  7. SQL Compact 데이터베이스를 App_Data 폴더에서 사이트의 App_Data 폴더로 복사합니다. 사이트에서 다른 데이터베이스를 사용하는 경우 해당 연결 문자열을 응용 프로그램의 Web.Config 파일에 추가합니다.

페이지에서 뷰로 이동

ASP.NET MVC 사이트의 초기 설정을 완료하면 기존 사이트의 핵심인 페이지를 마이그레이션할 준비가 됩니다. Web Pages에서는 페이지(.[cs/vb]html)에 해당 페이지에 필요한 태그, 비즈니스 논리 및 모든 데이터 액세스가 포함되어 있습니다. ASP.NET MVC로 마이그레이션하는 작업에서 가장 중요한 과정은 각 페이지를 분할하여 컨트롤러 작업(비즈니스 논리), 데이터 액세스 클래스(데이터 액세스), 그리고 뷰(태그)로 나누는 것입니다.

먼저 사이트의 레이아웃을 마이그레이션해야 합니다. 레이아웃 페이지는 Web Forms 및 ASP.NET MVC의 마스터 페이지와 비슷하며 사이트의 구조를 지정하는 파일입니다. Web Pages와 ASP.NET MVC 3(Razor 뷰 엔진을 사용하는 경우)은 모두 동일한 레이아웃 하위 시스템을 사용하므로 마이그레이션의 이 과정은 수월합니다. 사진 갤러리 사이트의 경우 루트 _SiteLayout.cshtml 파일에 사이트 구조가 포함되어 있습니다. 내용을 복사한 다음 ASP.NET MVC 사이트로 이동합니다. Views/Shared/_Layout.cshtml에 있는 레이아웃 파일을 열고 _SiteLayout.cshtml의 내용을 붙여 넣습니다.

이 단계가 완료되면 _Layout.cshtml에서 몇 가지 부분을 변경해야 합니다. 먼저, 스타일시트에 대한 링크를 ASP.NET MVC 응용 프로그램의 새로운 위치(~/Content/Site.css instead of ~/Styles/Site.css)로 변경합니다. 둘째, @Page.Title을 @ViewBag.Title로 변경해야 합니다. 두 가지 모두 사이트에서 페이지의 디스플레이 또는 다른 데이터를 포함하는 동적 형식의 개체이며, 짐작할 수 있겠지만 Page는 Web Pages에 사용되고, ViewBag은 ASP.NET MVC에 사용됩니다.

_Layout.cshtml에서 변경해야 하는 마지막 사항은 모든 페이지를 ASP.NET MVC로 마이그레이션할 때 주의해야 하는 사항이기도 합니다. _Layout.cshtml은 @Href 호출을 사용하여 URL을 페이지에 주입합니다. 정적 콘텐츠(스크립트, CSS 등)를 참조하는 호출의 경우 이러한 항목을 변경할 필요가 없습니다. 그러나 사이트의 페이지를 가리키는 @Href 호출은 모두 변경해야 합니다. 이러한 호출은 마이그레이션 이후에도 작동하지만 정적 URL을 가리키고 있기 때문입니다. ASP.NET MVC에서는 뷰가 렌더링될 때 URL을 생성하는 데 ASP.NET 라우팅을 사용하는 것이 좋습니다. 이렇게 하면 링크가 사이트에 하드코드되지 않고 깨끗하고 안정적으로 경로 테이블에 연결됩니다.

즉, 다음과 같은 링크가 있다면 변경해야 합니다.

<div id="banner">
  <p class="site-title">
    <a href="@Href("~/")">Photo Gallery</a>
  </p>
...
</div>

대신에 @Url.RouteUrl 또는 @Url.Action을 사용해야 합니다.

<div id="banner">
  <p class="site-title">
    <a href="@Url.Action("Default", "Gallery")">Photo Gallery</a>
  </p>
...
</div>

사이트 레이아웃을 이동한 후에는 페이지에서 뷰로 마이그레이션을 시작할 수 있습니다. Web Pages 응용 프로그램에 RenderPage 호출을 통해 실행되는 .cshtml 페이지가 있는 경우, 사이트 수준 페이지라면 Views/Shared로 이동하고, Account와 같은 컨트롤러에서 공유되는 페이지라면 해당하는 Views 하위 폴더로 이동합니다. 이러한 부분 페이지를 호출하는 각 페이지를 모두 새 위치를 반영하도록 업데이트해야 합니다.

나머지 페이지는 모두 뷰 아래에 컨트롤러를 기준으로 구성한 폴더로 이동해야 합니다. Web Pages 사이트에는 컨트롤러라는 개념이 없기 때문에 마이그레이션 중에 컨트롤러를 도입해야 합니다. 다행스럽게도 사진 갤러리 응용 프로그램을 보면 컨트롤러 구조의 형태가 잘 드러나며 여러분의 사이트에서 따라 할 수 있는 좋은 예를 보여 줍니다.

예를 들어 사진 갤러리 템플릿 사이트에서는 페이지를 그룹으로 묶기 위한 Account, Gallery, Photo, Tag 및 User라는 폴더가 있습니다. 각 폴더에는 해당 그룹과 관련된 몇 가지 기능을 활성화하는 페이지들이 포함되어 있습니다. 예를 들어 Account 폴더에는 사이트 로그인 및 로그아웃, 그리고 사용자 등록과 관련된 페이지가 포함되어 있습니다. Gallery 폴더에는 갤러리 목록 페이지, 새 갤러리를 추가하기 위한 페이지, 그리고 갤러리에서 사진을 보기 위한 페이지가 포함되어 있습니다. 나머지 폴더도 비슷한 방법으로 구성됩니다. 이러한 구조는 Web Pages 사이트에 필수는 아니지만 ASP.NET MVC로의 마이그레이션이 원활해지는 효과가 있습니다. 이렇게 하면 각 폴더가 깔끔하게 컨트롤러에 매핑되고 각 each .cshtml 파일이 작업과 뷰로 매핑됩니다.

Account 폴더와 여기에 속한 페이지 3개(Login, Logout 및 Register)를 ASP.NET MVC 응용 프로그램의 Views 디렉터리로 이동하는 것부터 시작하겠습니다. ASP.NET MVC의 용어에 따르면 페이지는 응용 프로그램에서 해당 위치에 의해 즉시 뷰가 됩니다. 그러나 사용자가 요청할 때 응용 프로그램이 이러한 뷰를 제공하려면 컨트롤러와 작업이 필요하므로 아직 끝난 것은 아닙니다.

컨트롤러 도입

MVC 관례에 따라 Views 아래에 Account 폴더가 있다는 것은 AccountController라는 컨트롤러가 있어야 한다는 것을 의미하므로, 다음 단계는 Controllers 폴더 아래에 컨트롤러를 만드는 것입니다. 마우스 오른쪽 단추를 클릭하고 추가 | 컨트롤러를 선택합니다. 이제 이 빈 컨트롤러에 우리 응용 프로그램으로 가져온 각 .cshtml 페이지 맨 위에 있는 논리를 포함할 작업 메서드를 만들 수 있습니다.

먼저 그림 1의 코드를 포함하는 Login.cshtml을 먼저 처리하겠습니다.

그림 1 Login.cshtml에 포함된 비즈니스 논리

Page.Title = "Login";
if (IsPost) {
  var email = Request["email"];
  if (email.IsEmpty()) {
    ModelState.AddError(
      "email", "You must specify an email address.");
  }
  var password = Request["password"];
  if (password.IsEmpty()) {
    ModelState.AddError(
      "password", "You must specify a password.");
  }

  if (ModelState.IsValid) {
    var rememberMe = Request["rememberMe"].AsBool();
    if (WebSecurity.Login(email, password, rememberMe)) { 
      string returnUrl = Request["returnUrl"];        
      if (!returnUrl.IsEmpty()) {
        Context.RedirectLocal(returnUrl);
      } else{
        Response.Redirect("~/");
      }
    } else {
      ModelState.AddFormError(
        "The email or password provided is incorrect.");
    }
  }
}

여기에서 두 가지 시나리오가 처리됩니다. 첫째는 사용자가 로그인 페이지를 처음으로 로드한 경우입니다. 이 시나리오에서 페이지는 자체 제목 및 태그로 직접 전환을 설정합니다. 둘째는 IsPost 조건에 포함되어 있으며 사용자가 로그인 양식을 완료하고 로그인 단추를 누르면 실행되는 논리를 나타냅니다.

ASP.NET MVC에서 컨트롤러에 빈 양식을 처리하는 것과 제출을 처리하는 것의 두 가지 작업 메서드를 만들어서 빈 양식 전달과 양식 제출 수락을 처리합니다. 첫 번째 작업은 페이지 제목을 설정하고 로그인 뷰를 반환하며, 두 번째 작업은 IsPost 조건 내의 논리를 포함합니다. 그림 2에 이러한 작업이 나와 있습니다. 이러한 두 작업을 추가한 다음에는 Login.cshtml에서 헤더 코드를 삭제합니다.

그림 2 로그인 컨트롤러 작업

public ActionResult Login() {
  ViewBag.Title = "Login";
  return View();
}

[HttpPost]
public ActionResult Login(string email, string password, 
  bool? rememberMe, string returnUrl) {
  if (email.IsEmpty())
    ModelState.AddModelError("email", 
      "You must specify an email address.");
  if (password.IsEmpty())
    ModelState.AddModelError("password", 
      "You must specify a password.");
  if (!ModelState.IsValid)
    return View();
  if (WebSecurity.Login(email, password, 
    rememberMe.HasValue ? rememberMe.Value : false)) {
    if (!string.IsNullOrEmpty(returnUrl))
      return Redirect(returnUrl);
    return RedirectToAction("Default", "Gallery");
  }

  ModelState.AddModelError("_FORM", 
    "The email or password provided is incorrect");
  return View();
}

원래 페이지와 결과 작업 메서드 간에는 두 가지 중요한 차이점이 있습니다. 우선, IsPost 조건이 필수가 아니라는 것을 알 수 있습니다. ASP.NET MVC에서는 두 번째 Login 작업 메서드를 만들고 [HttpPost] 특성을 지정하여 로그인 페이지에 대한 게시 작업을 만들었습니다. 첫 번째 Login 메서드는 ViewBag.Title 속성을 설정하고 ViewResult를 반환한 다음 Views/Account에서 Login.cshtml이라는 뷰 페이지를 찾는 간단한 작업을 수행합니다.

두 번째로 알 수 있는 것은 게시 작업에 여러 매개 변수가 포함되어 있고 원래 페이지에 사용되던 Request 호출이 모두 사라졌다는 것입니다. 로그인 양식의 필드 이름(email, password 및 rememberMe)과 일치하는 매개 변수를 메서드에 넣으면 ASP.NET MVC 기본 모델 바인더를 통해 이러한 항목을 작업에 대한 매개 변수로 전달되도록 할 수 있으며, 우리가 Request 개체를 호출할 필요가 없으므로 논리가 간결해집니다.

마지막으로 Web Pages와 ASP.NET MVC 응용 프로그램에는 유효성 검사가 처리되는 방법과 리디렉션이 수행되는 방법에 몇 가지 작은 차이점이 있습니다. 우리의 Web Pages 사이트에서 양식 데이터가 잘못되었을 때 페이지에 통지하는 데 사용하는 호출은 ModelState.AddError 및 .AddFormError입니다. ASP.NET MVC 응용 프로그램에서는 약간 다른 ModelState.AddModelError를 사용하며 페이지를 모두 변경해야 합니다. 리디렉션의 경우 Web Pages 사이트에서는 사용자를 다시 라우팅하기 위해 Response.Redirect를 호출합니다. ASP.NET MVC에서는 컨트롤러 작업이 ActionResult를 반환해야 하므로, 동일한 결과를 얻을 수 있도록 return RedirectToRoute(“Default”)를 호출했습니다.

로그인 페이지를 마이그레이션한 다음에는 곧바로 Logout.cshtml을 처리할 수 있습니다. Web Pages에서 Logout.cshtml과 같이 작업을 수행하고 사용자를 리디렉션하는 일부 페이지의 경우에는 논리를 포함하고 태그는 포함하지 않습니다.

@{
  WebSecurity.Logout();
  Response.Redirect("~/");
}

ASP.NET MVC에서는 이 작업을 수행할 Logout 작업을 추가할 수 있습니다.

public ActionResult Logout() {
  WebSecurity.Logout();
  return RedirectToAction("Default", "Gallery");
}

뷰는 페이지의 기능이 아닌 시각적인 요소만 나타내며 사용자를 로그아웃하고 리디렉션을 처리하는 작업을 만들었으므로 응용 프로그램에서 Logout.cshtml 뷰를 삭제할 수 있습니다.

지금까지 계정 페이지를 Views/Account 폴더로 복사하고, 계정 페이지에 대한 요청을 처리하는 AccountController를 만들고, 로그인 및 로그아웃 시나리오를 처리할 작업 메서드를 구현하여 계정 페이지를 뷰로 전환하는 과정을 수행했습니다. 이제 사이트를 빌드하고 실행한 다음 브라우저의 제목 표시줄에 Account/Login을 추가할 수 있습니다. 기본 홈 페이지는 아직 구현되지 않은 Gallery/Default를 가리키므로 표시되지 않습니다.

다음에 설명할 사이트의 다른 기능은 Web Pages 사이트의 App_Code 디렉터리에 포함되어 있는 코드와 도우미입니다. 마이그레이션을 시작할 때 이 디렉터리 전체를 ASP.NET MVC 응용 프로그램으로 옮기고 프로젝트에 포함시킬 수 있습니다. 이 디렉터리에 코드 파일(.cs 또는 .vb)이 포함되어 있는 경우 App_Code에 그대로 유지하거나 다른 곳으로 옮길 수 있습니다. 두 경우 모두 각 파일의 빌드 작업 속성을 콘텐츠가 아닌 컴파일로 변경해야 합니다. 디렉터리에 @helper 메서드 선언을 포함한 .cshtml 파일이 있는 경우, 이를 그대로 두고 ASP.NET MVC 응용 프로그램에서 활용할 수 있습니다.

Web Pages 사이트의 나머지 부분에서는 각 View 폴더의 컨트롤러를 만드는 비슷한 과정에 따라서 각 페이지의 작업을 만들고 각 페이지의 헤더 코드를 하나 이상의 작업으로 이동하면 됩니다. 그러면 얼마 지나지 않아서 모든 페이지를 컨트롤러 작업과 뷰로 깔끔하게 분리할 수 있게 됩니다. 그런데 아직 이 기사에서 MVC 패턴의 한 부분인 모델에 대해서는 이야기하지 않았습니다.

데이터 액세스를 리포지토리 클래스로 마이그레이션

각각의 페이지의 비즈니스 논리를 하나 이상의 컨트롤러 작업으로 옮기는 과정은 비교적 쉬운 편이지만 데이터 액세스는 예외입니다. 페이지 중에는 로그인 및 로그아웃 페이지와 비슷하며 데이터 액세스를 수행하지 않고 약간의 논리만 포함하는 경우도 있지만, 사실 대부분의 Web Pages 사이트는 데이터베이스를 사용합니다.

Account/Register.cshtml 페이지가 한 예입니다. 사용자가 등록 양식을 완료하고 등록을 클릭하면 페이지는 그림 3에 나오는 두 번의 데이터베이스 호출을 수행합니다.

그림 3 Register.cshtml 데이터베이스 논리

var db = Database.Open("PhotoGallery");
      
var user = db.QuerySingle("SELECT Email FROM 
UserProfiles WHERE LOWER(Email) = LOWER(@0)", email);
       
if (user == null) {      
  db.Execute(
    "INSERT INTO UserProfiles (Email, DisplayName, Bio) 
    VALUES (@0, @1, @2)", email, email, "");

  try {
    WebSecurity.CreateAccount(email, password);
    WebSecurity.Login(email, password);
    Response.Redirect("~/");
  } catch (System.Web.Security.MembershipCreateUserException e) {
    ModelState.AddFormError(e.ToString());
  }
} else {
  ModelState.AddFormError("Email address is already in use.");
}

등록 페이지는 먼저 PhotoGallery 데이터베이스를 열고 데이터베이스를 나타내는 WebMatrix.Data.Database 개체를 반환합니다. 그런 다음 페이지는 개체를 사용하여 사용자가 제공한 값으로 기존 전자 메일 주소를 확인합니다. 주소가 존재하지 않으면 새 UserProfile 레코드를 생성하며 WebSecurity 멤버 자격 공급자를 사용하여 사용자에 대한 계정을 생성합니다.

WebMatrix.Data에 대한 참조를 추가하고 로컬 복사 속성을 true로 설정했다면 다른 변경 없이 이 데이터베이스 논리를 사용할 수 있으며, 사이트가 정상적으로 작동합니다. 마이그레이션을 진행하는 동안 사이트 작동을 유지하려는 경우 이것이 전략적으로 유리한 단계일 수 있습니다.

그러나 이 기사에서는 한 단계 나아가서 ASP.NET MVC 응용 프로그램을 처음부터 작성할 때와 마찬가지로 데이터 액세스를 포함하는 추가 개체를 만들겠습니다. 컨트롤러와 데이터 액세스 논리를 분리하는 데 사용할 수 있는 패턴은 많습니다. 사진 갤러리에는 리포지토리 패턴을 사용할 것이며, 데이터 액세스를 리포지토리 클래스로 추상화함으로써 이 논리를 캡슐화하고 정식 Model 개체 또는 장기적으로 Entity Framework와 같은 ORM(개체 관계 매핑) 시스템에 가해질 영향을 최소화하도록 선택할 수 있습니다.

먼저 응용 프로그램에서 리포지토리 폴더와 AccountRepository.cs라는 간단한 클래스를 만드는 것으로 시작하겠습니다. 그런 다음 그림 4에 나오는 것처럼 Register 작업에서 각 데이터베이스 호출을 단계적으로 수행하고 해당 논리를 우리의 리포지토리로 옮길 수 있습니다.

그림 4 AccountRepository

public class AccountRepository {
  readonly Database _database;
  public AccountRepository() {
    database = Database.Open("PhotoGallery");
  }

  public dynamic GetAccountEmail(string email) { 
    return _database.QuerySingle(
      "SELECT Email FROM UserProfiles 
      WHERE LOWER(Email) = LOWER(@0)", email);
  }
 
  public void CreateAccount(string email) {
    _database.Execute(
      "INSERT INTO UserProfiles 
      (Email, DisplayName, Bio) VALUES (@0, @1, @2)", 
      email, email, "");
  }
}

Database.Open에 대한 호출을 리포지토리의 생성자에 추가했으며, 계정 전자 메일을 찾는 메서드와 계정을 생성하는 메서드를 작성했습니다.

GetAccountEmail의 반환 형식은 dynamic임을 알 수 있습니다. WebMatrix.Data의 쿼리 메서드 중에는 dynamic 또는 IEnumerable<dynamic>을 반환하는 것이 많으며 여러분의 리포지토리에서도 유지할 수만 있다면 이러한 관행을 사용하지 않을 이유가 없습니다. 

그림 5에는 AccountRespository를 사용하는 새로운 Register 메서드가 나옵니다.

그림 5 AccountRepository를 사용하는 Register 작업

[HttpPost]
public ActionResult Register(string email, string password, 
  string confirmPassword) {

  // Check Parameters (omitted)

  if (!ModelState.IsValid)
    return View();
 
  var db = new AccountRepository();
  var user = db.GetAccountEmail(email);
 
  if (user == null) {
    db.CreateAccount(email);
 
    try {
      WebSecurity.CreateAccount(email, password);
      WebSecurity.Login(email, password);
      return RedirectToAction("Default", "Gallery");
    }
    catch (System.Web.Security.MembershipCreateUserException e) {
      ModelState.AddModelError("_FORM", e.ToString());
    }
  }
  else {
    ModelState.AddModelError("_FORM", 
      "Email address is already in use.");
  }
 
  return View();
}

dynamic 반환 형식 사용은 괜찮은 선택이며 완전한 ASP.NET MVC 응용 프로그램으로 준비하고 실행하기 위한 마이그레이션 중에는 오히려 현명한 선택이라고 할 수 있습니다. ASP.NET MVC 응용 프로그램에서는 강력한 형식 모델이 필수가 아니므로 데이터 모델의 코드 기반 정의가 필요하지 않다면 이 전략을 활용할 수 있습니다. 동적 모델 ASP.NET MVC 응용 프로그램의 컨트롤러 논리와 뷰는 정상적으로 작동하지만 한 가지 예외가 있습니다.

여러분의 Web Pages 응용 프로그램에서는 양식 필드가 표준 태그로 명시적으로 정의되었음을 알 수 있습니다.

<input type="text" />
<input type="submit" />
...

ASP.NET MVC에서 양식 컨트롤을 사용하는 데는 Html.TextBox 또는 Html.TextBoxFor와 같은 Html 도우미 메서드를 사용하는 것이 더 좋은 방법입니다. 이러한 메서드는 뷰로 전달된 모델을 사용하여 현재 값을 설정하고 양식 유효성 검사를 처리하기 때문입니다. 이러한 도우미 메서드를 마이그레이션 후 뷰에서 사용하려면 강력한 형식의 모델 개체를 도입하고 리포지토리에서 dynamic 형식을 제거해야 합니다. 이러한 도우미 메서드에서는 동적 모델에서는 작동하지 않습니다.

사이트 URL 보존

사이트 URL은 중요합니다. 사이트 상태에 관계없이 검색 엔진, 설명서, 통신, 테스트 스크립트와 같은 여러 외부 원본이 기존 URL에 따라 달라집니다. 이러한 종속성이 있기 때문에 마이그레이션 상황이라도 URL을 임의로 변경해서는 안 됩니다.

기존 URL을 보존하기 위해 ASP.NET 라우팅을 사용하는 것을 고려할 수 있습니다. ASP.NET 라우팅은 요청을 원활하게 하고 올바른 리소스(이 경우에는 컨트롤러와 작업)로 연결하도록 지원합니다. Web Pages는 ASP.NET MVC와는 다른 라우팅 시스템을 사용하므로 기존 URL을 보존하기 위해 어느 정도 시간을 투자해야 합니다.

Web Pages 응용 프로그램은 확장자가 없는 URL을 처리할 수 있습니다. 예를 들어 다음 두 URL은 동일한 페이지로 확인됩니다.

http://mysite/Gallery/Default.cshtml

http://mysite/Gallery/Default

그러나 ASP.NET MVC 응용 프로그램에서는 .cshtml 확장자를 사용하는 첫 번째 URL이 제대로 처리되지 않습니다. 사이트 전체에서 확장자 없는 URL을 사용하면 검색 엔진과 다른 종속 사이트에서도 동일하게 작업하여 사이트의 마이그레이션에 대한 영향을 최소화할 수 있습니다. 그러나 확장자가 있는 기존 URL을 처리해야 하는 경우에는 ASP.NET MVC 응용 프로그램에서 경로를 만들어 이러한 URL이 깨지지 않도록 할 수 있습니다.

사진 갤러리 응용 프로그램의 기본 경로를 예로 들어 보겠습니다.

routes.MapRoute(
  "Default", 
  "{controller}/{action}/{id}", 
  new { controller = "Home", 
    action = "Index", id = "" } 
);

시스템에서 기존 URL을 지원하려면 Global.asax 파일에서 이 정의 위에 있는 경로 테이블에 경로를 추가해야 합니다. 다음은 이러한 정의의 예입니다.

routes.MapRoute(
  "LegacyUrl",
  "{controller}/{action}.cshtml/{id}",
  new { controller = "Gallery", 
    action = "Default", id = "" }
);

이 경로 항목에서는 기존 Web Pages 사이트 구조가 깔끔하게 컨트롤러/작업 구조와 매핑되는 경우 .cshtml 확장자를 포함하는 URL이 처리되고 적절한 컨트롤러와 작업으로 전송됩니다.

마이그레이션을 계획할 때는 기존 URL을 지원하기 위해 기본 경로를 변경해야 하거나 추가 경로가 필요할 수 있다는 것을 기억하십시오. 한편, 기존 URL을 깨려는 경우 사용자를 위한 영구적인 리디렉션을 처리하는 작업을 추가해야 합니다.

요약

Web Pages에서 ASP.NET MVC로 마이그레이션의 영향을 요약하기 위해 사진 갤러리 사이트의 이전 및 이후 구조를 살펴보겠습니다. 그림 6에서 왼쪽에는 WebMatrix의 Web Pages 사이트 구조가 나옵니다. 그리고 오른쪽에서 ASP.NET MVC로 마이그레이션을 완료한 후의 사이트가 나옵니다. 구조는 달라졌지만 최종 결과는 상당히 익숙하게 느껴질 것입니다.

Application Layout Before and After

그림 6 이전 및 이후 응용 프로그램 레이아웃

현재 ASP.NET 개발자는 Web Forms, Web Pages 및 ASP.NET MVC의 세 가지 프레임워크 옵션을 선택할 수 있습니다. 각각의 프레임워크는 다른 장점이 있지만, 하나를 선택하더라도 다른 프레임워크를 이용할 수 있으며 나중에 다른 프레임워크로 마이그레이션하는 것도 가능합니다. 그리고 세 가지 모두 ASP.NET에 기반을 두고 있으므로 기술적인 이유 때문에 다른 프레임워크로 전환하는 것은 아닙니다. 한편, 이동하기로 결정한 경우 Web Pages와 ASP.NET MVC는 서로 비슷하므로 NuGet, Razor, Web Deploy, IIS Express 및 SQL Compact와 같은 기술을 수정 없이 계속 사용할 수 있습니다.

Web Pages를 사용하여 응용 프로그램을 작성했고 이동하는 것이 좋다고 생각된다면, Web Pages에서 ASP.NET MVC로의 마이그레이션은 가장 마찰이 적은 경로이며, 특히 Web Pages 사이트에서 페이지를 그룹별로 폴더에 배치하고, 모든 리소스에 상대 URL을 사용하며, 모든 비즈니스 논리를 각 페이지 맨 위에 넣는 등의 사전 디자인 결정을 내린 경우에는 더욱 과정이 수월합니다. 이 경우 마이그레이션이 필요한 시기가 되면 Web Pages에서 ASP.NET MVC로의 마이그레이션을 부드럽고 매끄럽게 수행할 수 있습니다.

이 기사에서 사용된 기법 및 기술과 그밖에 다양한 내용을 bit.ly/WebMatrixToMVC에서 볼 수 있습니다.

Brandon Satrom은 텍사스 주 오스틴 외각에서 Microsoft 선임 개발자 전도사로 일하고 있습니다. 블로그(userInexperience.com) podcasts(DeveloperSmackdown.com), 그리고 Twitter(twitter.com/BrandonSatrom)에서 그를 만날 수 있습니다.

Clark Sell은 시카고 외각에서 Microsoft 선임 개발자 전도사로 일하고 있습니다. 블로그(csell.net), podcasts(DeveloperSmackdown.com), 그리고 Twitter(twitter.com/csell5)에서 그를 만날 수 있습니다.

이 문서를 검토하는 데 많은 도움을 주신 기술 전문가인 Phil HaackErik Porter에게 감사 인사를 전합니다.