파트 9: ASP.NET Core MVC - 유효성 검사 추가하기

등록일시: 2016-07-11 08:00,  수정일시: 2016-09-26 14:06
조회수: 7,642
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
이번 파트에서는 Movie 모델에 유효성 검사 로직을 추가하고, 사용자가 영화 정보를 생성하거나 수정할 때마다 유효성 검사 규칙이 적용되게 만들어보겠습니다.

이번 파트에서는 Movie 모델에 유효성 검사 로직을 추가하고, 사용자가 영화 정보를 생성하거나 수정할 때마다 유효성 검사 규칙이 적용되게 만들어보겠습니다.

중복배제(DRY, Don't Repeat Yourself) 원칙 유지하기

MVC의 가장 기본적인 설계 원칙 중 한 가지는 "중복배제(DRY, Don't Repeat Yourself)" 입니다. ASP.NET MVC에서 권장하는 작업 방식은 기능이나 동작을 단 한 번만 지정하면 응용 프로그램 전반에 그 효과가 일괄적으로 반영되는 것입니다. 이런 방식을 사용하면 작성해야 할 코드의 양이 줄어들 뿐만 아니라 코드 자체의 오류도 적어지고, 테스트가 쉬워지며, 유지보수도 쉬워집니다.

MVC와 Entity Framework Code First가 지원해주는 유효성 검사 기능이 바로 중복배제 원칙의 좋은 실제 사례입니다. 즉, 모델 클래스 한 곳에서만 선언적 유효성 검사 규칙을 지정하면 응용 프로그램의 모든 곳에 선언된 규칙이 적용됩니다.

지금부터 영화 정보 응용 프로그램에 유효성 검사 기능을 추가하는 방법을 살펴보도록 하겠습니다.

Movie 모델에 유효성 검사 규칙 추가하기

모델 클래스가 작성되어 있는 Movie.cs 파일을 엽니다. 클래스나 속성에 선언적으로 지정할 수 있는 내장 유효성 검사 어트리뷰트들의 모음은 DataAnnotations 네임스페이스에서 제공됩니다. (이 네임스페이스에는 DataType 같이 서식만 지정할 뿐 아무런 유효성 검사 기능도 제공해주지 않는 서식 관련 어트리뷰트들도 함께 포함되어 있습니다.)

다음 코드에서 볼 수 있는 것처럼, 기본으로 제공되는 내장 Required, StringLength, RegularExpression, Range 유효성 검사 어트리뷰트들을 적용해서 Movie 클래스를 수정합니다.

public class Movie
{
    public int ID { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; }

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

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
    [StringLength(5)]
    public string Rating { get; set; }
}

이런 유효성 검사 어트리뷰트들은 해당 어트리뷰트가 적용된 모델 속성에 강제하려는 동작들을 지정합니다. 가령 Required 어트리뷰트나 MinimumLength 속성이 설정된 StringLength 어트리뷰트가 지정된 속성은 반드시 값이 입력되야만 유효한 것으로 인식되는데, 사용자가 이 유효성 검사를 통과하기 위해서 공백을 입력하는 것까지 제한하지는 못합니다. RegularExpression 어트리뷰트는 입력 가능한 문자를 제한하기 위한 용도로 사용됩니다. 이를테면, 이번 예제 코드에서 Genre 필드와 Rating 필드에는 반드시 문자만 입력할 수 있습니다 (공백, 숫자 또는 특수 문자는 허용되지 않습니다). 그리고 Range 어트리뷰트는 지정된 속성에 입력할 수 있는 값을 특정 범위 내로 제한합니다. StringLength 어트리뷰트는 문자열 속성의 최대 길이를 지정하며, 필요한 경우 선택적으로 최소 길이를 지정할 수도 있습니다. 또한 값 형식은 (decimal, int, float, DateTime 등) 기본적으로 값이 존재해야만 유효한 것으로 간주되므로 [Required] 어트리뷰트를 명시적으로 지정하지 않아도 동일한 효과를 갖습니다.

이렇게 지정한 유효성 검사 규칙들은 ASP.NET에 의해서 자동으로 적용되어 응용 프로그램을 더욱 견고하게 만들어줍니다. 그로 인해서, 특정 사례에 대한 유효성 검사를 누락해서 부주의하게 잘못된 데이터가 데이터베이스에 저장되는 경우를 방지하는 효과도 얻게 됩니다.

역주: 엄밀하게 말한다면 유효성 검사 어트리뷰트를 추가하는 작업 역시 모델 클래스를 변경하는 행위이므로, 다음 절을 살펴보기 전에 이전 파트에서처럼 먼저 마이그레이션 작업을 수행하는 것이 보다 올바른 작업 방식입니다. 그러면 각 속성에 적용된 어트리뷰트의 규칙들이 적절한 형태로 데이터베이스 테이블에 반영되는데, 가령 Rating 컬럼의 데이터 형식 같은 경우 nvarchar(5)로 변경됩니다. 그러나 마이그레이션 작업을 수행하지 않더라도 본 자습서를 살펴보는 데는 별다른 문제가 없습니다.

MVC의 유효성 검사 오류 UI

이제 응용 프로그램을 실행하고 Movies 컨트롤러로 이동합니다.

그런 다음, Create New 링크를 클릭하고 새로운 영화 정보를 추가해봅니다. 이때, 의도적으로 유효하지 않은 임의의 값들을 폼에 입력합니다. 그러면 클라이언트 측 jQuery 유효성 검사 기능이 오류를 감지하는 즉시 오류 메시지를 출력할 것입니다.

노트

여러분이 사용 중인 로케일에 따라 Price 필드에 소수점이나 쉼표(",")를 정상적으로 입력할 수 없는 경우도 있습니다. 쉼표를 소수점으로 사용하는 비-영어권 로케일과 비 US-English 날짜 형식에 대해서 jQuery 유효성 검사 기능을 정상적으로 지원하려면 응용 프로그램이 국제화를 지원하도록 별도의 작업을 수행해야만 합니다. 구체적인 방법은 추가 자료 절을 참고하시기 바랍니다. 일단 지금은 간단히 10 같은 정수를 입력하시면 됩니다.

폼이 자동으로 유효하지 않은 값이 입력된 각 필드에 적절한 유효성 검사 오류 메시지를 렌더하는 것을 확인할 수 있습니다. 이 오류들은 클라이언트 측과 (JavaScript와 jQuery를 이용하는) 서버 측 (사용자가 JavaScript를 비활성화시킨 경우에) 모두에서 발생하게 됩니다.

이 방식의 가장 중요한 장점은 유효성 검사 UI를 활성화시키기 위해서 MoviesController 클래스나 Create.cshtml  뷰의 코드를 단 한줄도 변경할 필요가 없다는 점입니다. 심지어 본 자습서 시리즈의 이전 파트들을 통해서 만들었던 모든 컨트롤러와 뷰들도 본문에서 유효성 검사 어트리뷰트로 Movie 모델 클래스의 속성들에 지정한 유효성 검사 규칙들을 자동으로 인식합니다. 가령 Edit 액션 메서드의 유효성 검사 기능을 테스트해보면 어떠한 추가 작업을 하지 않았음에도 동일한 유효성 검사 기능이 적용됨을 확인할 수 있습니다.

폼의 데이터는 클라이언트 측 유효성 검사 오류들이 모두 해결될 때까지 서버로 전송되지 않습니다. 이 점은 HTTP Post 메서드에 중단점을 설정해보거나, Fiddler 도구 또는 IE의 F12 개발자 도구를 이용해서 직접 확인해볼 수 있습니다.

Create 뷰와 Create 액션 메서드의 유효성 검사 수행방식

컨트롤러나 뷰의 코드를 전혀 수정하지 않았음에도 불구하고 어떻게 이런 유효성 검사 UI가 생성되는지 궁금할 것입니다. 다음 예제 코드는 두 버전의 Create 메서드들을 보여주고 있습니다.

// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for 
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,Genre,Price,ReleaseDate,Title,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View(movie);
}

이 두 버전의 메서드들 중, 첫 번째 HTTP GET Create 액션 메서드는 초기 Create 폼을 출력하고, [HttpPost] 어트리뷰트가 적용된 두 번째 Create 액션 메서드는 폼 전송을 처리합니다. [HttpPost] 버전의 두 번째 Create 메서드는 제출된 데이터에 유효성 검사 오류가 존재하는지 여부를 검사하기 위해서 ModelState.IsValid를 호출하는데, 바로 이 메서드가 호출될 때 개체에 적용된 모든 유효성 검사 어트리뷰트들이 평가됩니다. 그리고 그 결과에 따라 영화 정보 개체에 유효성 검사 오류가 존재하면 Create 메서드가 폼을 다시 출력하고, 오류가 존재하지 않으면 메서드가 새로운 영화 정보를 데이터베이스에 저장합니다. 다만 본 자습서의 영화 응용 프로그램에서는 클라이언트 측에서 유효성 검사 오류가 감지되면 아예 서버로 폼이 전송되지 않기 때문에, 현재 상태로는 두 번째 Create 메서드가 호출되는 일은 결코 일어나지 않습니다. 따라서 브라우저에서 JavaScript를 비활성화시키는 경우에만 클라이언트 측 유효성 검사 자체가 비활성화되어, HTTP POST Create 메서드가 ModelState.IsValid 메서드를 호출함으로써 서버 측에서 영화 정보의 유효성 검사 오류 여부를 확인할 수 있습니다.

직접 [HttpPost] Create 메서드에 중단점을 설정해보면, 유효성 검사 오류가 발생할 때 클라이언트 측 유효성 검사로 인해서 폼 데이터가 제출되지 않기 때문에 메서드 자체가 호출되지 않는 것을 확인할 수 있습니다. 반면 브라우저에서 JavaScript를 비활성화시키면 오류가 존재하는 폼이 서버로 제출되고 중단점에서 실행이 중지될 것입니다. 다시 말해서 JavaScript의 지원 없이도 완벽한 유효성 검사를 수행할 수 있는 것입니다. 다음 그림은 Internet Explorer에서 JavaScript를 비활성화시키는 방법을 보여줍니다.

그리고 다음 그림은 FireFox 브라우저에서 JavaScript를 비활성화시키는 방법을 보여주고 있습니다.

마지막으로 다음 그림은 Chrome 브라우저에서 JavaScript를 비활성화시키는 방법을 보여주고 있습니다.

이렇게 JavaScript를 비활성화시킨 다음 유효하지 않은 데이터를 전송하고 디버거로 중단점을 살펴보시기 바랍니다.

다음 코드는 본 자습서의 이전 파트에서 스캐폴드로 만든 Create.cshtml  뷰 템플릿의 일부분을 보여주고 있습니다. 이 뷰 템플릿은 폼이 최초에 출력될 때, 그리고 유효성 검사 오류로 인해서 다시 출력될 때 위의 액션 메서드들에 의해서 사용됩니다.

<form asp-action="Create">
    <div class="form-horizontal">
        <h4>Movie</h4>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="Genre" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Genre" class="form-control" />
                <span asp-validation-for="Genre" class="text-danger" />
            </div>
        </div>

        @*나머지 필드들의 마크업은 생략되었습니다.*@
        
        <div class="form-group">
            <label asp-for="Rating" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Rating" class="form-control" />
                <span asp-validation-for="Rating" class="text-danger" />
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
</form>

이 코드에서 Input 태그 헬퍼(Input Tag Helper)DataAnnotations 어트리뷰트들을 기반으로 클라이언트 측 jQuery 유효성 검사에 필요한 HTML 어트리뷰트들을 만들어냅니다. 그리고 유효성 검사 태그 헬퍼(Validation Tag Helper)가 유효성 검사 오류를 출력합니다. 더 자세한 정보는 모델 유효성 검사(Model Validation) 문서를 참고하시기 바랍니다.

이 접근방식의 가장 큰 장점은 컨트롤러나 Create 뷰 템플릿 중 그 어느 쪽도, 적용될 실제 유효성 검사 규칙이나 출력될 특정 오류 메시지에 관해서 알고 있어야 할 필요가 없다는 점입니다. 유효성 검사 규칙과 오류 메시지는 Movie 클래스에만 지정됩니다. 그러면 동일한 유효성 검사 규칙이 자동으로 Edit 뷰 뿐만 아니라 모델을 생성하거나 수정하는 모든 다른 뷰 템플릿들에 적용됩니다.

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

DataType 어트리뷰트 사용하기

다시 Movie.cs  파일로 돌아가서 Movie 클래스를 살펴봅니다. 이 클래스에서 사용되고 있는 System.ComponentModel.DataAnnotations 네임스페이스는 내장 유효성 검사 어트리뷰트들의 모음과 함께 서식 관련 어트리뷰트들을 제공해줍니다. 이미 우리는 ReleaseDate 속성과 Price 속성에 DataType 열거형 값을 적용해봤습니다. 다음 코드는 적절한 DataType 어트리뷰트가 적용된 ReleaseDate 속성과 Price 속성을 보여주고 있습니다.

[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

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

그러나 DataType 어트리뷰트는 뷰 엔진에게 단지 데이터의 서식을 지정하기 위한 힌트만 알려줄 뿐입니다 (그리고 추가적으로 URL에 대한 <a> 태그나 전자메일에 대한 <a href="mailto:EmailAddress.com"> 태그를 생성해주는 등의 어트리뷰트도 제공해줍니다.) 데이터 서식의 유효성을 검증하기 위한 목적으로 이 어트리뷰트 대신 대신 RegularExpression 어트리뷰트를 사용할 수도 있습니다. 일반적으로 DataType 어트리뷰트는 데이터베이스의 내장 형식보다 조금 더 구체적인 데이터 서식을 지정하기 위해서 사용되며, 유효성 검사 어트리뷰트가 아닙니다. 가령 이번 예제 코드에서 ReleaseDate 속성의 경우에는 날짜와 시간 전부 대신 날짜만 담는 것이 그 목적입니다. DataType 열거형은 Date, Time, PhoneNumber, Currency, EmailAddress 등 다양한 데이터 형식들을 제공해줍니다. 또한 DataType 어트리뷰트를 이용해서 응용 프로그램이 자동으로 특정 데이터 형식에 최적화된 기능을 제공하도록 활성화시킬 수도 있습니다. 예를 들어서 DataType.EmailAddress 열거형을 지정하면 자동으로 mailto: 링크가 생성되고, DataType.Date 열거형을 지정하면 HTML5를 지원하는 브라우저에서는 날짜 선택기가 제공됩니다. 이런 일이 가능한 이유는 DataType 어트리뷰트가 HTML 5를 지원하는 브라우저가 인식 가능한 HTML 5 data- ("데이터 대시"라고 읽습니다) 어트리뷰트를 만들어내기 때문입니다. 그러나 다시 강조하지만 DataType 어트리뷰트는 어떠한 유효성 검사 기능도 제공해주지 않습니다.

그리고 DataType.Date 열거형 값이 출력되는 날짜의 서식 그 자체를 지정하는 것은 아니라는 점에 주의하시기 바랍니다. 기본적으로 날짜 필드는 서버의 CultureInfo에 기반한 기본 서식에 따라서 출력됩니다.

날짜의 서식을 명시적으로 지정하기 위해서는 DisplayFormat 어트리뷰트를 사용하면 됩니다:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }

이 코드에서 ApplyFormatInEditMode 속성은 편집 텍스트 상자에 값이 출력될 때도 지정된 서식을 적용하도록 지시합니다. (그러나 필드에 따라서는 이런 기능이 불필요한 경우도 있습니다. 예를 들어서, 통화 값의 경우 편집 텍스트 상자에까지 통화 기호가 함께 나타나는 것을 원하는 경우는 많지 않습니다.)

일반적으로 DisplayFormat 어트리뷰트만 단독으로 사용하는 것보다는 DataType 어트리뷰트를 함께 사용하는 것이 바람직합니다. 왜냐하면 DataType 어트리뷰트는 데이터를 화면에 렌더하는 직접적인 방법이 아니라 데이터의 의미(Semantics)를 전달해주며, 다음과 같이 DisplayFormat 어트리뷰트가 지원하지 않는 이점들을 제공해주기 때문입니다:

  • 브라우저에서 HTML5 기능을 사용할 수 있습니다 (달력 컨트롤 지원, 지역에 적합한 통화 기호, 이메일 링크, 일부 클라이언트 측 입력 유효성 검사 등).
  • 기본적으로 브라우저가 자체적으로 로케일에 따라 올바른 서식으로 데이터를 렌더합니다.
  • DataType 어트리뷰트를 사용하면 MVC가 데이터를 렌더할 올바른 필드 템플릿을 선택하는데 도움이 됩니다. (DisplayFormat 어트리뷰트는 일반적인 문자열 템플릿을 사용합니다). 보다 자세한 정보는 Brad Wilson의 ASP.NET MVC 2 Templates 블로그 포스트를 참고하시기 바랍니다. (비록 이 포스트는 MVC 2를 대상으로 작성된 문서지만 현재 버전의 ASP.NET MVC에서도 여전히 유효한 정보를 담고 있습니다.)

노트

DateTime 형식에 Range 어트리뷰트를 적용하면 jQuery 유효성 검사 기능이 정상적으로 동작하지 않습니다. 가령 다음 코드는 입력된 날짜가 지정한 범위 내에 존재하더라도 항상 클라이언트 측 유효성 검사 오류가 출력됩니다:

[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

결과적으로 DateTime 형식에 Range 어트리뷰트를 적용하려면 jQuery 날짜 유효성 검사를 비활성화시켜야 할 필요가 있습니다. 일반적으로 모델에서 날짜를 일일이 점검하는 것은 좋은 방식이 아니므로, DateTime 형식에 대해 Range 어트리뷰트를 사용하는 것은 권장되지 않습니다.

다음 코드와 같이 어트리뷰트들을 한 줄로 연결할 수도 있습니다:

public class Movie
{
    public int ID { get; set; }

    [StringLength(60, MinimumLength = 3)]
    public string Title { get; set; }

    [Display(Name = "Release Date"), DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$"), Required, StringLength(30)]
    public string Genre { get; set; }

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

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$"), StringLength(5)]
    public string Rating { get; set; }
}

다음 파트에서는 자동으로 생성된 Details 메서드와 Delete 메서드를 검토해보도록 하겠습니다.