login register Sysop! about ME  
qrcode
    최초 작성일 :    2012년 07월 03일
  최종 수정일 :    2012년 07월 03일
  작성자 :    songgun
  편집자 :    songgun (송 원석)
  읽음수 :    24,035

강좌 목록으로 돌아가기

필자의 잡담~

본문은 ASP.NET MVC 4 자습서 시리즈의 마지막 편입니다. 본래 세 편으로 구성된 원문을 그대로 분할하거나 중간에 자르기에는 문맥이 애매해져서 불가피하게 한 편으로 편역하게 되었습니다.

본 자습서를 모두 살펴보신 뒤에는, 자습서의 끝 부분에 소개된 보다 높은 수준의 자습서를 계속해서 살펴보시거나, 관련 서적을 통해서 한층 더 깊은 ASP.NET MVC의 세계에 빠져들어보시길 권해드립니다.

본문은 ASP.net의 공식 MVC 관련 자습서인 Adding a New Field to the Movie Model and TableAdding Validation to the Model, 그리고 Examining the Details and Delete Methods를 편역한 글입니다. 마이크로소프트의 공식 번역 문서가 아니며 태오 사이트 MS 컬럼 번역팀에서 번역한 내용입니다. 그렇기에, 일부 오역이나 오타가 존재할 수 있는 점 미리 양해를 드립니다. 원문에 대한 모든 저작권은 마이크로소프트에 있으며, 컬럼 내용과 관련한 질의 문답 역시 원문 사이트에 문의하시는 것을 추천드립니다.

Movie 모델 및 테이블에 새로운 필드 추가하기
이번에는 모델 클래스를 일부 변경한 다음, 그 결과를 데이터베이스 스키마에 반영하는 방법을 살펴보도록 하겠습니다.


Movie 모델에 Rating 속성 추가하기
먼저, 기존 Movie 클래스에 새로운 Rating 속성을 추가하겠습니다. Models\Movie.cs 파일을 연 다음, 다음과 같은 Rating 속성을 추가합니다:
public string Rating { get; set; }
이 작업을 마치고 나면 Movie 클래스의 모습은 다음과 비슷할 것입니다:
public class Movie
{
    public int ID { get; set; }
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public string Genre { get; set; }
    public decimal Price { get; set; }
    public string Rating { get; set; }
}
그런 다음, Build > Build Movie 메뉴를 선택해서 응용 프로그램을 다시 컴파일합니다.

이제, Model 클래스를 변경했으므로 \Views\Movies\Index.cshtml 뷰 템플릿과 \Views\Movies\Create.cshtml 뷰 템플릿을 변경해서 추가된 Rating 속성을 브라우저에 출력해야 합니다.

먼저, \Views\Movies\Index.cshtml 파일을 열고, Price 컬럼 헤더 뒷 부분에 <th>Rating</th> 컬럼 헤더를 추가합니다. 그리고, @item.Rating 값을 렌더할 <td> 컬럼을 템플릿 끝 부분에 추가합니다. 다음은 변경을 마친 Index.cshtml 뷰 템플릿의 모습을 보여주고 있습니다:
@model IEnumerable<MvcMovie.Models.Movie>

@{ 
    ViewBag.Title = "Index"; 
} 

<h2>Index</h2> 
 
<p> 
    @Html.ActionLink("Create New", "Create") 
</p> 
<table> 
    <tr> 
        <th> 
            @Html.DisplayNameFor(model => model.Title) 
        </th> 
        <th> 
            @Html.DisplayNameFor(model => model.ReleaseDate) 
        </th> 
        <th> 
            @Html.DisplayNameFor(model => model.Genre) 
        </th> 
        <th> 
            @Html.DisplayNameFor(model => model.Price) 
        </th> 
        <th>
            @Html.DisplayNameFor(model => model.Rating)
        </th> 
        <th></th> 
    </tr> 

@foreach (var item in Model) { 
    <tr> 
        <td> 
            @Html.DisplayFor(modelItem => item.Title) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.ReleaseDate) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.Genre) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.Price) 
        </td> 
        <td> 
            @Html.DisplayFor(modelItem => item.Rating)
        </td>
        <td> 
            @Html.ActionLink("Edit", "Edit", new { id=item.ID }) | 
            @Html.ActionLink("Details", "Details", new { id=item.ID }) | 
            @Html.ActionLink("Delete", "Delete", new { id=item.ID }) 
        </td> 
    </tr> 
} 

</table>
이번에는 \Views\Movies\Create.cshtml 파일을 열고, 폼의 마지막 부분에 다음과 같은 마크업을 추가합니다. 이 마크업은 텍스트 박스를 렌더해주는데, 새로운 영화 정보를 생성할 때, 이 텍스트 박스를 사용해서 등급을 입력할 수 있습니다.
<div class="editor-label"> 
    @Html.LabelFor(model => model.Rating) 
</div> 
<div class="editor-field"> 
    @Html.EditorFor(model => model.Rating) 
    @Html.ValidationMessageFor(model => model.Rating) 
</div>

모델과 데이터베이스 스키마 간의 차이점 관리하기
지금까지 새로운 Rating 속성 지원을 위해서 응용 프로그램의 코드를 수정했습니다.

다시 응용 프로그램을 실행한 다음, /Movies URL로 이동해보겠습니다. 그러면, 다음과 같은 두 가지 오류 중 한 가지가 발생하는 것을 보게 될 것입니다:

출처 : http://i1.asp.net/umbraco-beta-media/37723/ContextChangedError.png


출처 : http://i1.asp.net/umbraco-beta-media/37730/WindowsLiveWriter_ImplementingEditDetailsandDelete_11047_BackingError_thumb_1.png

이런 오류들이 발생하는 이유는, 지금 막 변경한 응용 프로그램의 Movie 모델 클래스와 데이터베이스의 Movie 테이블 스키마가 서로 일치하지 않기 때문입니다. (데이터베이스의 테이블에는 아직 Rating 속성에 해당하는 컬럼이 존재하지 않습니다.)

이전 강좌에서 살펴본 것처럼, Entity Framework Code First를 이용해서 데이터베이스를 자동생성하면, 생성된 데이터베이스의 스키마와 그에 대응하는 모델 클래스 사이에 동기화가 필요한지 여부를 지속적으로 판단하기 위한 테이블이 데이터베이스에 기본적으로 함께 추가됩니다. 그리고, 만약 동기화가 되어 있지 않은 것이 발견되면, 지금처럼 Entity Framework가 오류를 발생시키게 됩니다. 결과적으로 뒤늦게 런타임에 가서야 발견되었을 지도 모를 (모호한 오류로 인한) 문제점을 개발 시점에 손쉽게 추적할 수 있는 것입니다. 바로, 이런 동기화 점검(Sync-Checking) 기능으로 인해서 방금 살펴본 오류 메시지가 출력된 것입니다.

이 오류를 해결하기 위한 접근방법은 두 가지가 존재합니다:
  1. Entity Framework가 자동으로 데이터베이스를 드랍시킨 다음, 새로운 모델 클래스 스키마를 기준으로 데이터베이스를 재생성하도록 만드는 방법입니다. 이 방법은 모델과 데이터베이스를 신속하게 변경할 수 있으므로, 테스트 데이터베이스에서 개발이 한창 진행 중일 때 매우 편리합니다. 그러나, 단점은 데이터베이스에 존재하던 기존 데이터들이 사라지게 되므로, 절대로 운영 데이터베이스에서는 사용하면 안된다는 것입니다!
  2. 명시적으로 직접 데이터베이스의 스키마를 변경하여 모델 클래스와 일치시키는 방법입니다. 이 방법의 장점은 기존 데이터를 유지할 수 있다는 점입니다. 직접 데이터베이스를 변경하거나, 데이터베이스 변경 스크립트를 작성해서 수행할 수 있습니다.
본 자습서에서는 첫 번째 방법을 사용해서, 모델 클래스가 변경될 때마다 Entity Framework Code First가 자동으로 데이터베이스를 재생성하도록 만들어 보겠습니다.


변경된 모델에 따라 자동으로 데이터베이스 재생성하기
그러면 지금부터 응용 프로그램의 모델이 변경될 때마다, Code First가 자동으로 데이터베이스를 드랍시키고 재생성할 수 있도록 응용 프로그램을 변경해보겠습니다.

경고   데이터베이스를 자동으로 드랍시키고 재생성하는 이 방법은, 개발 데이터베이스나 테스트 데이터베이스를 사용할 때만 사용해야하며, 실제 데이터를 담고 있는 운영 데이터베이스를 대상으로는 절대 사용하시면 안됩니다. 운영 서버에 이 방식을 적용하면 데이터를 잃어버리게 될 수도 있습니다.

디버거를 중지하고, Solution Explorer에서 Models 폴더를 마우스 오른쪽 버튼으로 클릭한 다음, Add를 클릭하고, 다시 New Item을 선택합니다.

출처 : http://i1.asp.net/umbraco-beta-media/37659/AddNewClass.png

잠시 후 Add New Item 대화 상자가 나타나면, Class를 선택한 다음, 클래스의 이름을 "MovieInitializer"로 지정합니다. 그리고, 다음의 코드로 MovieInitializer 클래스의 내용을 대체합니다:
using System;
using System.Collections.Generic;
using System.Data.Entity;

namespace MvcMovie.Models {
    public class MovieInitializer : DropCreateDatabaseIfModelChanges<MovieDBContext> {
        protected override void Seed(MovieDBContext context) {
            var movies = new List<Movie> {

                new Movie { Title = "When Harry Met Sally",
                            ReleaseDate=DateTime.Parse("1989-1-11"),
                            Genre="Romantic Comedy",
                            Rating="R",
                            Price=7.99M },

                new Movie { Title = "Ghostbusters",
                            ReleaseDate=DateTime.Parse("1984-3-13"),
                            Genre="Comedy",
                            Rating="R",
                            Price=8.99M },

                new Movie { Title = "Ghostbusters 2",
                            ReleaseDate=DateTime.Parse("1986-2-23"),
                            Genre="Comedy",
                            Rating="R",
                            Price=9.99M },

                new Movie { Title = "Rio Bravo",
                            ReleaseDate=DateTime.Parse("1959-4-15"),
                            Genre="Western",
                            Rating="R",
                            Price=3.99M },
            };

            movies.ForEach(d => context.Movies.Add(d));
        }
    }
}
MovieInitializer 클래스는 모델 클래스가 변경되면, 모델이 사용하는 데이터베이스를 자동으로 드랍시키고 재생성하도록 지시합니다. 이 코드에서처럼 DropCreateDatabaseIfModelChanges 이니셜라이져를 상속 받으면 스키마가 변경된 경우에만 데이터베이스가 재생성됩니다. 또는, DropCreateDatabaseAlways 이니셜라이저를 상속 받아서, 응용 프로그램 도메인에서 컨텍스트가 처음 사용될 때마다 항상 데이터베이스를 다시 생성하고 데이터를 초기화시킬 수도 있습니다. DropCreateDatabaseAlways 이니셜라이저 방식은 특정 통합 테스트 시나리오에서 아주 유용합니다. 그리고, 이 MovieInitializer 클래스에 작성된 Seed 메서드는, 데이터베이스가 생성(또는 재생성) 될 때마다 자동으로 데이터베이스에 추가될 약간의 기본 데이터들을 지정합니다. 이 메서드를 작성해 놓으면, 모델이 변경될 때마다 매번 수작업으로 데이터를 입력할 필요 없이 원하는 테스트 데이터로 편리하게 데이터베이스를 채울 수 있습니다.

클래스 정의를 마쳤으므로, 이제 매번 응용 프로그램이 실행될 때마다 모델 클래스와 데이터베이스의 스키마가 다른지 검토할 수 있도록 MovieInitializer 클래스를 구성할 차례입니다. 검토를 해 본 결과, 동기화가 필요한 상태라면 이니셜라이저를 실행해서 데이터베이스를 모델과 일치하도록 재생성하고, 기본 데이터를 입력하게 됩니다.

먼저, Global.asax 파일을 엽니다:

출처 : http://i1.asp.net/umbraco-beta-media/37731/WindowsLiveWriter_ImplementingEditDetailsandDelete_11047_Global_asax_sm_thumb_2.png

Global.asax 파일에는 프로젝트의 응용 프로그램 전체를 정의하는 클래스 정의와, 응용 프로그램이 처음 시작될 때 실행되는 Application_Start 이벤트 핸들러가 들어 있습니다.

다음과 같이 Application_Start 메서드의 시작 부분에 Database.SetInitializer 호출 코드를 추가합니다:
protected void Application_Start()
{
    Database.SetInitializer<MovieDBContext>(new MovieInitializer());
    AreaRegistration.RegisterAllAreas();

    // Use LocalDB for Entity Framework by default
    Database.DefaultConnectionFactory = new SqlConnectionFactory("Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True");

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    BundleTable.Bundles.RegisterTemplateBundles();
}
그리고, 빨간색 밑줄이 쳐진 부분을 (MovieDBContextMovieInitializer) 마우스 오른쪽 버튼으로 클릭한 다음, Resolve > using MvcMovie.Models; 선택합니다.

출처 : http://i1.asp.net/umbraco-beta-media/37727/p7_resolveData.Entity.png

또는, 파일 상단에 직접 다음과 같은 using 문을 추가해도 됩니다. 다만 이 using 문은 MovieInitializer 클래스가 실제로 위치한 네임스페이스를 참조해야 합니다:
using MvcMovie.Models;              // MovieInitializer
이렇게 구성을 해놓으면, 응용 프로그램이 시작될 때, 지금 막 추가한 Database.SetInitializer 구문으로 인해서 MovieDBContext 인스턴스와 관련된 데이터베이스의 스키마와 모델 클래스가 일치하지 않는 경우, 자동으로 데이터베이스가 삭제되었다가 재생성되게 됩니다. 그리고, 앞에서 설명한 것처럼 그 과정 중에 데이터베이스가 MovieInitializer 클래스에 지정된 기본 데이터로 채워지게 됩니다.

이제 Global.asax 파일을 닫습니다.

응용 프로그램을 다시 시작한 다음, /Movies URL로 이동해봅니다. 그러면, 응용 프로그램이 시작될 때, 모델의 구조와 데이터베이스 스키마가 더 이상 일치하지 않는다는 사실이 감지될 것입니다. 그에 따라, 새로운 모델 구조와 일치하도록 데이터베이스가 재생성되고 데이터베이스가 기본 영화 데이터로 채워지게 됩니다:

출처 : http://i1.asp.net/umbraco-beta-media/37725/p7_rating.PNG

역주   만약, 본문에서 설명한 대로 동작하지 않고 계속해서 오류가 발생한다면, 화면 우측 하단의 작업 표시줄에 나타나 있는 IIS Express 아이콘을 마우스 오른쪽 버튼으로 클릭한 다음, MvcMovie > 사이트 중지를 선택해서 사이트 자체를 다시 시작합니다. Global.asax 파일에 대한 변경은 웹 응용 프로그램 자체를 다시 시작해야 반영되는 경우가 많습니다.

이번에는 Create New 링크를 클릭해서 새로운 영화 정보를 추가해보십시요. 이제는 등급도 지정할 수 있다는 점에 주목하시기 바랍니다.

출처 : http://i1.asp.net/umbraco-beta-media/37728/WindowsLiveWriter_AddingValidationtotheModel_96DF_7_CreateRioII_thumb_1.png

적절한 정보들을 입력한 다음, Create를 클릭하면, 등급 정보가 포함된 새로운 영화 정보가 목록에 나타나는 것을 확인할 수 있을 것입니다:

출처 : http://i1.asp.net/umbraco-beta-media/37729/WindowsLiveWriter_AddingValidationtotheModel_96DF_7_ourNewMovie_SM_thumb.png

마지막으로 Edit 뷰 템플릿에도 Rating 필드를 추가해줍니다.

지금까지 모델 개체를 변경하는 방법과, 모델의 해당 변경 사항을 데이터베이스에 반영하는 방법을 살펴봤습니다. 그리고, 특정 시나리오에서 새로 생성된 데이터베이스에 기본 데이터를 채워 넣을 수 있는 방법도 살펴봤습니다. 계속해서 모델 클래스에 다채로운 유효성 검사 로직을 추가하는 방법과 특정 업무 로직을 강제하는 방법을 살펴보도록 하겠습니다.


모델에 유효성 검사 추가하기
이번에는 Movie 모델에 유효성 검사 로직을 추가해 보겠습니다. 그리고, 사용자가 응용 프로그램을 이용해서 영화 정보를 생성하거나 수정하려고 시도할 때마다, 항상 유효성 검사 규칙이 적용되도록 만들어 보겠습니다.


DRY(Don't Repeat Yourself) 원칙 유지하기
마이크로소프트의 개발자들이 ASP.NET MVC을 설계할 때 중요하게 여긴 몇 가지 방침이 있습니다. 그 방침들 중 하나는 바로 "반복 작업은 하지 않는다(DRY)"는 것입니다. ASP.NET MVC에서는 기능이나 동작을 단 한 번만 작성하면, 응용 프로그램의 모든 영역에 그 결과가 반영되는 형태의 작업방식이 권장됩니다. 이런 형태의 작업방식은 작성해야 할 코드의 양을 줄여줄뿐만 아니라, 작성된 코드 자체의 오류도 감소되는 경향을 보여주며, 유지보수에도 용이합니다.

그리고, 지금부터 살펴보게 될 ASP.NET MVC와 Entity Framework Code First가 함께 제공해주는 유효성 검사 기능이야말로 DRY 원칙이 반영된 멋진 실제 사례로 평가할 수 있습니다. 모델 클래스, 단 한 곳에서만 유효성 검사 규칙을 선언적으로 지정하면, 그 규칙을 응용 프로그램의 모든 곳에 적용할 수 있는 것입니다.

그러면, 지금부터 영화 응용 프로그램에서 어떻게 유효성 검사 지원의 장점을 얻을 수 있는지, 그 방법을 살펴보도록 하겠습니다.


Movie 모델에 유효성 검사 규칙 추가하기
먼저, Movie 클래스에 몇 가지 유효성 검사 로직을 추가해보겠습니다. Movie.cs 파일을 연 다음, 다음과 같이 파일 상단에 System.ComponentModel.DataAnnotations 네임스페이스를 참조하는 using 구문을 추가합니다:
using System.ComponentModel.DataAnnotations;
네임스페이스 경로에 System.Web이 포함되어 있지 않다는 점에 유의하시기 바랍니다. 이 DataAnnotations 네임스페이스는 클래스나 속성에 선언적으로 적용할 수 있는 내장 어트리뷰트들의 모음을 제공해줍니다.

지금부터 Movie 클래스를 수정해서, RequiredStringLength, 그리고 Range 등과 같은 내장 유효성 검사 어트리뷰트들의 이점을 활용해보도록 하겠습니다. 다음 예제 코드를 살펴보면 어떤 식으로 이 어트리뷰트들을 지정해야 하는지 쉽게 이해할 수 있습니다.
public class Movie {
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Required]
    public string Genre { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [StringLength(5)]
    public string Rating { get; set; }
}
이렇게 유효성 검사 어트리뷰트는 해당 어트리뷰트가 적용된 모델 속성에 여러분이 강제하고자 하는 동작을 지정합니다. 예를 들어서, Required 어트리뷰트를 지정하면 그 속성은 반드시 값을 갖고 있어야만 유효한 것으로 간주됩니다. 따라서, Movie 클래스는 Title, ReleaseDate, Genre, Price 속성에 값이 존재하는 경우에만 유효한 상태인 것으로 판단됩니다. Range 어트리뷰트는 지정된 속성의 값을 특정 범위로 제한합니다. StringLength 어트리뷰트는 문자열 속성의 최대 길이를 지정하며, 필요한 경우 선택적으로 최소값을 지정할 수도 있습니다. 기본적으로 고유 형식들은 (decimal, int, float, DateTime 등) 항상 값이 존재해야만 유효한 것으로 간주되므로, Required 어트리뷰트를 따로 지정해줄 필요가 없습니다.

그리고, 지금처럼 Code First를 사용하는 경우에는, 응용 프로그램이 데이터베이스에 변경 사항을 저장하기 직전에 자동으로 모델 클래스에 지정된 유효성 검사 규칙들이 적용됩니다. 가령, 다음 코드는 Movie 클래스의 일부 필수 속성에 값이 존재하지 않고, Price 속성의 값이 0 이므로 (유효 범위를 벗어나므로), SaveChanges 메서드가 호출될 때 예외가 던져집니다.
MovieDBContext db= new MovieDBContext();

Movie movie = new Movie();
movie.Title = "Gone with the Wind";
movie.Price = 0.0M;

db.Movies.Add(movie); 
db.SaveChanges();        // <= 유효성 검사 예외가 던져집니다.
이렇게 .NET 프레임워크가 자동으로 유효성 검사 규칙을 적용해주므로 응용 프로그램이 보다 강력해집니다. 결과적으로, 유효성 검사의 수행을 잊거나 부주의로 잘못된 데이터를 데이터베이스에 저장하는 일이 방지되는 것입니다.

변경이 완료된 Movie.cs 파일의 완전한 코드는 다음과 같습니다:
using System;
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models {
    public class Movie {
        public int ID { get; set; }

        [Required]
        public string Title { get; set; }

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Required]
        public string Genre { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        public decimal Price { get; set; }

        [StringLength(5)]
        public string Rating { get; set; }
    }

    public class MovieDBContext : DbContext {
        public DbSet<Movie> Movies { get; set; }
    }
}
ASP.NET MVC 유효성 검사 UI
응용 프로그램을 다시 실행한 다음, /Movies URL로 이동해봅니다.

그리고, Create New 링크를 클릭해서 새로운 영화 정보를 추가해보십시요. 폼에 유효하지 않은 임의의 값을 입력한 다음, Create 버튼을 클릭해봅니다.

출처 : http://i1.asp.net/umbraco-beta-media/37738/WindowsLiveWriter_AddingaNewFieldtotheMovieModelandTable_E60A_8_validationErrors_thumb.png

그러면, 자동으로 유효하지 않은 값을 갖고 있는 폼의 텍스트 박스가 빨간색 테두리로 강조되고, 해당 텍스트 박스 옆에 적절한 유효성 검사 오류 메시지가 나타나는 모습을 확인할 수 있습니다. 이 오류는 클라이언트 측과 (자바스크립트를 이용한) 서버 측에서 (사용자가 자바스크립트를 비활성화시킨 경우) 모두 발생하게 됩니다.

이 기능의 장점은 유효성 검사 UI를 활성화시키기 위해서, MoviesController 클래스나 Create.cshtml 뷰의 코드를 변경할 필요가 전혀 없다는 점입니다. 이전 단계들에서 여러분이 만든 컨트롤러와 뷰는, 유효성 검사 어트리뷰트를 적용해서 Movie 모델 클래스에 지정한 유효성 검사 규칙들을 자동으로 인식합니다.

눈치가 빠른 분들은, 폼을 제출하거나 (Create 버튼을 클릭해서) 텍스트 박스에 텍스트를 입력했다가 다시 지우기 전까지는, Required 어트리뷰트가 지정된 Title이나 Genre 속성에 대한 유효성 검사가 즉시 수행되지 않는다는 것을 알아채셨을 겁니다. Required 어트리뷰트만 지정되고 다른 어트리뷰트가 지정되지 않았으며, 최초에 빈 상태로 출력되는 필드들은 (Create 뷰의 필드들처럼) 다음과 같은 과정을 통해서 유효성 검사를 발생시킬 수 있습니다:
  1. 탭을 눌러서 필드에 들어갑니다.
  2. 임의의 텍스트를 입력합니다.
  3. 탭을 눌러서 필드에서 나갑니다.
  4. Shift+탭을 눌러서 다시 필드에 들어갑니다.
  5. 텍스트를 제거합니다.
  6. 탭을 눌러서 필드에서 나갑니다.
이런 일련의 동작을 수행하면 제출 버튼을 클릭하지 않아도 Required 유효성 검사가 수행됩니다. 그리고, 단순히 모든 필드를 비워둔 채로, 제출 버튼을 클릭해보면 클라이언트 측 유효성 검사가 수행될 것입니다. 폼 데이터는 클라이언트 측 유효성 검사 오류가 모두 사라질 때까지는 서버로 전송되지 않습니다. HTTP Post 메서드에 중단점을 설정해보거나, 피들러 또는 IE9의 F12 개발자 도구 등을 사용해서 이를 테스트 해볼수 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37736/p8_required.PNG


Create 뷰와 Create 액션 메서드에서 유효성 검사가 수행되는 방식
아마도 여러분은 어떻게 컨트롤러나 뷰의 코드를 전혀 수정하지 않고서도 유효성 검사 UI가 생성되는지 궁금할 것입니다. 다음 목록은 MovieController 클래스의 Create 메서드를 보여주고 있습니다. 기존 코드와 다른 점이 전혀 없다는 사실을 확인할 수 있습니다.
//
// GET: /Movies/Create

public ActionResult Create()
{
    return View();
}

//
// POST: /Movies/Create

[HttpPost]
public ActionResult Create(Movie movie)
{
    if (ModelState.IsValid)
    {
        db.Movies.Add(movie); 
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(movie);
}
이 두 메서드 중에서, 첫 번째 Create 액션 메서드(HTTP GET)는 초기 Create 폼을 출력해주고, 두 번째 Create 액션 메서드(HTTP POST)는 폼 전송을 처리해줍니다. 그리고, 이 두 번째 Create 메서드(HttpPost 버전)에서는 전송된 영화 정보에 유효성 검사 오류가 존재하는지 여부를 검사하기 위해서 ModelState.IsValid를 호출하는데, 바로 이 메서드가 호출될 때, 개체에 적용되어 있는 모든 유효성 검사 어트리뷰트들이 평가됩니다. 그 결과에 따라, 영화 정보 개체에 유효성 검사 오류가 존재하면 폼이 다시 출력되고, 아무런 오류도 존재하지 않으면 새로운 영화 정보가 데이터베이스에 저장되게 됩니다. 그런데, 지금 테스트 중인 영화 예제의 경우, 클라이언트 측에서 유효성 검사 오류가 감지되면 서버로 폼이 전송되지 않으므로, 현재 상태에서는 두 번째 Create 메서드가 결코 호출되지 않습니다. 이런 경우, 브라우저에서 자바스크립트를 비활성화시켜서 클라이언트 측 유효성 검사 자체를 비활성화시키면, HTTP POST Create 메서드의 ModelState.IsValid가 호출되어 유효성 검사 오류 여부를 서버에서 확인할 수 있게 됩니다.

실제로 HttpPost Create 메서드에 중단점을 설정해보면, 유효성 검사 오류가 발생했을 때, 클라이언트 측 유효성 검사가 폼 데이터를 제출하지 않기 때문에, 메서드가 호출되지 않는 것을 직접 확인하실 수 있습니다. 브라우저에서 자바스크립트를 비활성화시키면, 오류가 포함된 상태로 폼이 제출되고, 중단점에서 실행이 중지됩니다. 자바스크립트 없이도 유효성 검사가 완벽하게 수행되는 것입니다. 다음 그림은 인터넷 익스플로러 브라우저에서 자바스크립트를 비활성화시키는 방법을 보여주고 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37734/p8_IE9_disableJavaScript.png


출처 : http://i1.asp.net/umbraco-beta-media/37735/p8_modelState_invalid.png

다음 그림은 파이어폭스 브라우저에서 자바스크립트를 비활성화시키는 방법을 보여주고 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37733/p8_FF_diableJavaScript.png

다음 그림은 크롬 브라우저에서 자바스크립트를 비활성화시키는 방법을 보여주고 있습니다.

출처 : http://i1.asp.net/umbraco-beta-media/37732/p8_chromeDisableJavaScript.png

그리고, 다음은 본 자습서에서 스캐폴딩으로 만들어진 Create.cshtml 뷰 템플릿입니다. 이 뷰 템플릿은 폼이 최초에 출력될 때, 그리고 유효성 검사 오류가 발생해서 다시 출력될 때, 위의 액션 메서드에 의해서 사용됩니다.
@model MvcMovie.Models.Movie 

@{ 
    ViewBag.Title = "Create"; 
} 

<h2>Create</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"></script> 

@using (Html.BeginForm()) { 
    @Html.ValidationSummary(true) 

    <fieldset>
        <legend>Movie</legend>

        <div class="editor-label"> 
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label"> 
            @Html.LabelFor(model => model.ReleaseDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.ReleaseDate)
            @Html.ValidationMessageFor(model => model.ReleaseDate)
        </div>

        <div class="editor-label"> 
            @Html.LabelFor(model => model.Genre)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Genre)
            @Html.ValidationMessageFor(model => model.Genre)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Price) 
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Price)
            @Html.ValidationMessageFor(model => model.Price)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Rating) 
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Rating)
            @Html.ValidationMessageFor(model => model.Rating)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div> 
    @Html.ActionLink("Back to List", "Index") 
</div>
이 뷰 템플릿에서 Html.EditorFor 도우미를 이용해서 Movie 클래스의 각 속성들에 대한 <input> 요소를 출력하고, 바로 그 뒤에 Html.ValidationMessageFor 도우미 메서드가 호출되는 방식을 주의해서 살펴보시기 바랍니다. 이 두 메서드들은 컨트롤러에서 뷰로 전달된 모델 개체(여기에서는 Movie 개체)를 대상으로 동작하며, 자동으로 모델에 적용되어 있는 유효성 검사 어트리뷰트를 파악한 다음, 적적한 오류 메시지 등을 출력해줍니다.

이 접근방식의 장점은, 컨트롤러나 Create 뷰 템플릿 모두, 실제 유효성 검사 규칙이나 출력되어야 할 지정된 오류 메시지에 대해서 전혀 알고 있을 필요가 없다는 점입니다. 유효성 검사 규칙과 오류 메시지는 Movie 클래스에만 지정됩니다.

나중에 유효성 검사 로직을 변경하고 싶다면, 모델 단 한 곳에서만 (이 예제의 경우, Movie 클래스) 유효성 검사 어트리뷰트를 변경하면 됩니다. 응용 프로그램의 다른 부분들에 대한 일관성 유지에 관해서는 전혀 걱정할 필요가 없습니다. 모든 유효성 검사 로직은 한 장소에서 정의되어 모든 곳에서 사용됩니다. 이 접근방식을 사용하면 코드를 대단히 깔끔하게 유지할 수 있으며, 유지보수와 개선이 쉬워집니다. 결과적으로 DRY 원칙을 완벽하게 따르게 되는 셈입니다.


Movie 모델에 포멧팅 추가하기
다시 Movie.cs 파일을 열고 Movie 클래스를 살펴보겠습니다. System.ComponentModel.DataAnnotations 네임스페이스는 기본적인 내장 유효성 검사 어트리뷰트 외에도 포멧팅 어트리뷰트들을 제공해줍니다. 가령, 이미 우리들은 ReleaseDate 필드와 Price 필드에 DataType 열거형 값을 적용했었습니다. 다음 코드는 DisplayFormat 어트리뷰트가 적절하게 적용된 ReleaseDate 속성과 Price 속성을 보여주고 있습니다.
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[DataType(DataType.Currency)]
public decimal Price { get; set; }
이 어트리뷰트 대신, 명시적으로 DataFormatString 값을 지정할 수도 있습니다. 다음 코드는 ReleaseDate 속성에 날짜 포멧팅 문자열("d")이 지정된 모습을 보여줍니다. 개봉일자에 시간이 포함되는 것을 원하지 않을 때 유용합니다.
[DisplayFormat(DataFormatString = "{0:d}")]
public DateTime ReleaseDate { get; set; }
그리고, 다음 코드는 Price 속성의 형식을 통화로 지정합니다.
[DisplayFormat(DataFormatString = "{0:c}")]
public decimal Price { get; set; }
다음은 변경사항이 반영된 완전한 Movie 클래스를 보여줍니다.
public class Movie {
    public int ID { get; set; }

    [Required]
    public string Title { get; set; }

    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Required]
    public string Genre { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [StringLength(5)]
    public string Rating { get; set; }
}
다시 응용 프로그램을 시작하고, Movies 컨트롤러로 이동해봅니다. 그러면, 개봉일자와 가격이 멋지게 포멧팅 된 것을 확인할 수 있을 것입니다.

출처 : http://i1.asp.net/umbraco-beta-media/37737/WindowsLiveWriter_AddingaNewFieldtotheMovieModelandTable_E60A_8_format_SM_9cb96bbf-d5c3-4e6e-b816-43bd43ee2b3b.png

역주   아마도 본 자습서에서 제공되는 거의 모든 날짜 및 통화 관련 포멧팅 화면 캡처들과 여러분이 실제로 테스트해 봤을때 나타나는 실제 화면의 포멧팅은 서로 다를 것입니다. 이는 여러분이 사용하고 있는 컴퓨터의 국가 및 언어 설정과 원문 작성자의 그것이 동일하지 않기 때문입니다. 이 문제를 해결하려면 ASP.NET MVC 4 자습서 - 만들면서 배우기 (III)로케일 관련 참고사항 사이드 바를 참고하시기 바랍니다.

그러면, 계속해서 자동으로 만들어진 Details 메서드와 Delete 메서드를 조금 더 자세하게 살펴보도록 하겠습니다.


Details 메서드 및 Delete 메서드 살펴보기
이번에는 자동으로 생성된 Details 메서드와 Delete 메서드를 조금 더 자세히 살펴보도록 하겠습니다.


Details 메서드 및 Delete 메서드 살펴보기
다시 Movie 컨트롤러를 열고, Details 메서드를 살펴보십시요.
public ActionResult Details(int id = 0)
{
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    return View(movie);
}
이와 같이 Code First를 사용하면 Find 메서드를 이용해서 손쉽게 데이터를 검색할 수 있습니다. 그리고, 보안과 관련하여 Details 메서드에 구현된 중요한 기능 한 가지는, 검색된 영화 정보를 이용해서 작업을 수행하기 전에, 항상 코드에서 Find 메서드의 검색 결과가 존재하는지부터 먼저 확인한다는 점입니다. 만약, 이 검사를 수행하지 않는다면, 악의적인 사용자가 링크에 의해서 만들어진 URL인 http://localhost:xxxx/Movies/Details/1http://localhost:xxxx/Movies/Details/12345 등의 URL로 변경해서 (실제로 존재하지 않는 아무 영화 정보나 지정해서) 사이트로부터 오류를 이끌어낼 수도 있습니다. 그런 경우, 검색된 영화 정보가 null인지 확인하지 않는다면, 데이터베이스 오류가 발생할 것이기 때문입니다.

그러면, 이번에는 Delete 메서드와 DeleteConfirmed 메서드를 살펴보겠습니다.
// GET: /Movies/Delete/5

public ActionResult Delete(int id = 0)
{
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    return View(movie);
}

//
// POST: /Movies/Delete/5

[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id = 0)
{
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    db.Movies.Remove(movie);
    db.SaveChanges();
    return RedirectToAction("Index");
}
이 코드에서 HTTP Get Delete 메서드는 지정된 영화 정보를 실제로 삭제하는 것이 아니라, 단지 영화 정보를 제출해서 (HttpPost) 삭제 작업을 수행할 수 있는 뷰를 반환할 뿐이라는 점에 주의하시기 바랍니다. 그 이유는 삭제 작업을 (또는, 수정 작업이나, 생성 작업을 비롯한 데이터 변경이 발생하는 모든 작업을) GET 요청에서 수행하면 보안상 취약점이 발생하기 때문입니다. 이 문제점에 대한 보다 자세한 정보는 Stephen Walther의 블로그 포스트인 ASP.NET MVC Tip #46 - Don't use Delete Links because they create Security Holes를 참고하시기 바랍니다.

그리고, 실제로 데이터를 삭제하는 HttpPost 메서드에는 HTTP POST 메서드에 고유한 시그니처 혹은 이름을 부여하기 위해서 DeleteConfirmed라는 이름이 지정되어 있는 것을 볼 수 있습니다. 이 두 메서드의 시그니처를 비교해보면 다음과 같습니다:
// GET: /Movies/Delete/5
public ActionResult Delete(int id = 0)

//
// POST: /Movies/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id = 0)
공통 언어 런타임(CLR, Common Language Runtime)에서 오버로드 된 메서드들은 시그니처가 유일해야만 합니다 (즉, 메서드 이름은 같을 수가 있지만, 매개변수의 목록은 달라야 합니다). 그런데, 이 예제의 경우, 두 개의 Delete 메서드(GET 버전과 POST 버전)가 필요한 상황인데 반해, 공교롭게도 필요한 두 메서드의 시그니처가 동일합니다. (두 메서드 모두 하나의 정수형 매개변수를 받습니다.)

이 문제를 해결하려면 몇 가지 작업이 필요합니다. 우선 두 메서드의 이름이 서로 달라야 합니다. 이 예제에서 스캐폴딩 메커니즘 역시 같은 방식을 따르고 있는 것을 확인할 수 있습니다. 그러나, 아직도 문제가 남아 있는데, ASP.NET이 URL을 액션 메서드와 맵핑할 때, 메서드 이름을 기준으로 처리하므로, 지금처럼 메서드 이름을 변경하면, 라우팅이 적절한 액션 메서드를 찾을 수가 없게 됩니다. 이 문제의 해결방법은, 위의 코드에서도 알 수 있는 것처럼, 바로 DeleteConfirmed 메서드에 지정되어 있는 ActionName("Delete") 어트리뷰트를 지정하는 것입니다. 이 어트리뷰트를 이용하면 /Delete/가 포함된 URL에 대한 POST 요청도 라우팅 시스템을 통해서 DeleteConfirmed 메서드로 원활하게 맵핑시킬 수 있습니다.

동일한 이름 및 시그니처로 인한 문제점을 해결할 수 있는 또 다른 보편적인 방법은, 아예 인위적으로 사용하지 않는 매개변수를 POST 메서드의 시그니처에 추가하는 것입니다. 예를 들어서, 어떤 개발자들은 다음 코드처럼 POST 메서드에 전달되는 FormCollection 형식의 매개변수를 추가한 다음, 그냥 이 매개변수를 무시해버립니다.
public ActionResult Delete(FormCollection fcNotUsed, int id = 0)
{
    Movie movie = db.Movies.Find(id);
    if (movie == null)
    {
        return HttpNotFound();
    }
    db.Movies.Remove(movie);
    db.SaveChanges();
    return RedirectToAction("Index");
}
마무리
이제 여러분은 SQL 서버 컴팩트 데이터베이스에 데이터를 저장하는 완벽한 ASP.NET 응용 프로그램을 갖게 되었습니다. 영화 정보를 생성하고, 읽고, 변경하고, 삭제할 수 있으며 검색도 가능합니다.

출처 : http://i1.asp.net/umbraco-beta-media/37680/WindowsLiveWriter_AddingaNewFieldtotheMovieModelandTable_E60A_8_format_SM_9cb96bbf-d5c3-4e6e-b816-43bd43ee2b3b.png

기초적인 본 자습서에서는 먼저 컨트롤러를 구현한 다음, 이 컨트롤러와 뷰를 연결하고, 하드코딩 된 데이터를 뷰에 전달해봤습니다. 그리고, 데이터 모델을 설계 및 생성하고, Entity Framework Code First를 사용해서 해당 데이타 모델로부터 실시간으로 데이터베이스를 생성한 다음, ASP.NET MVC 스캐폴딩 시스템을 이용해서 기초적인 CRUD 작업들을 위한 액션 메서드와 뷰들을 자동적으로 생성했습니다. 그런 다음, 검색 폼을 추가해서 사용자들이 데이터베이스를 검색할 수 있도록 만들었습니다. 새로운 데이터 컬럼을 추가하기 위해서 데이터베이스를 변경했으며, 그 데이터를 생성하고 출력하기 위해서 두 개의 페이지도 수정했습니다. 그리고, 데이터 모델에 DataAnnotations 네임스페이스의 어트리뷰트들을 적용해서 유효성 검사를 추가했으며, 그 결과 클라이언트와 서버 양쪽에서 유효성 검사가 수행되었습니다.

만약, 이 응용 프로그램을 배포해보고 싶다면, 먼저 로컬 IIS 7 서버에서 테스트를 해보는 것이 좋습니다. 웹 플랫폼 인스톨러 링크를 이용하면 ASP.NET 응용 프로그램을 위한 IIS 설정을 활성화시킬 수 있습니다. 그리고, 다음의 배포 관련 문서들도 살펴보시기 바랍니다:
MSDN의 ASP.NET 관련 기사들을 살펴보시려면 계속해서 중급 수준의 자습서인 Creating an Entity Framework Data Model for an ASP.NET MVC ApplicationMVC Music Store를 살펴보는 것이 좋습니다. 그리고, http://asp.net/mvc에서 제공되는 다양한 동영상과 자료들도 살펴보시기 바랍니다. ASP.NET MVC 포럼에 질문을 올리시는 것도 좋은 방법입니다.

그러면, 즐거운 시간이 되시기 바랍니다!

- Rick Anderson blogs.msdn.com/rickAndy


authored by

  quamdiu
  2013-01-06(10:29)
캐릭 이미지
잘보았습니다. 감사합니다.
  sisyphus2020
  2013-08-02(17:02)
캐릭 이미지
잘 봤습니다. 감사....
  insusu
  2014-10-07(16:01)
캐릭 이미지
잘 보고 갑니다~

 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})