파트 8: 모델에 유효성 검사 추가하기

등록일시: 2012-07-03 11:04,  수정일시: 2015-06-10 23:57
조회수: 5,217
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

이번에는 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 버튼을 클릭해봅니다.

8_validationErrors

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

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

눈치가 빠른 분들은, 폼을 제출하거나 (Create 버튼을 클릭해서) 텍스트 박스에 텍스트를 입력했다가 다시 지우기 전까지는, Required 어트리뷰트가 지정된 Title이나 Genre 속성에 대한 유효성 검사가 즉시 수행되지 않는다는 것을 알아채셨을 겁니다. Required 어트리뷰트만 지정되고 다른 어트리뷰트가 지정되지 않았으며, 최초에 빈 상태로 출력되는 필드들은 (Create 뷰의 필드들처럼) 다음과 같은 과정을 통해서 유효성 검사를 발생시킬 수 있습니다:

  1. 탭을 눌러서 필드에 들어갑니다.
  2. 임의의 텍스트를 입력합니다.
  3. 탭을 눌러서 필드에서 나갑니다.
  4. Shift+탭을 눌러서 다시 필드에 들어갑니다.
  5. 텍스트를 제거합니다.
  6. 탭을 눌러서 필드에서 나갑니다.

이런 일련의 동작을 수행하면 제출 버튼을 클릭하지 않아도 Required 유효성 검사가 수행됩니다. 그리고, 단순히 모든 필드를 비워둔 채로, 제출 버튼을 클릭해보면 클라이언트 측 유효성 검사가 수행될 것입니다. 폼 데이터는 클라이언트 측 유효성 검사 오류가 모두 사라질 때까지는 서버로 전송되지 않습니다. HTTP Post 메서드에 중단점을 설정해보거나, 피들러 또는 IE9의 F12 개발자 도구 등을 사용해서 이를 테스트 해볼수 있습니다.

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

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

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

그리고, 다음은 본 자습서에서 스캐폴딩으로 만들어진 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 컨트롤러로 이동해봅니다. 그러면, 개봉일자와 가격이 멋지게 포멧팅 된 것을 확인할 수 있을 것입니다.

8_format_SM

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

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