모델: 모델 유효성 검사

등록일시: 2016-08-12 08:00,  수정일시: 2016-08-30 06:22
조회수: 8,051
이 문서는 ASP.NET Core MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 몇 가지 기본적인 유효성 검사 어트리뷰트들의 사용 방법을 알아보고, 사용자 지정 유효성 검사와 클라이언트 측 유효성 검사, 그리고 원격 유효성 검사 같은 보다 고급의 시나리오에 관해서 간단하게 살펴봅니다.

모델 유효성 검사 소개

응용 프로그램은 데이터베이스에 데이터를 저장하기 전에 반드시 그 유효성을 검사해야 합니다. 데이터의 잠재적인 보안 위협 여부를 확인하고, 올바른 형식과 크기로 서식화되어 있는지 검증해야 하며, 지정한 규칙을 준수하고 있는지 살펴봐야 합니다. 유효성 검사는 생략할 수 있는 것처럼 보이기 쉽고 지루한 작업이지만 반드시 필요한 과정입니다. MVC에서 유효성 검사는 클라이언트 측과 서버 측 모두에서 수행됩니다.

다행스럽게도 .NET은 유효성 검사를 유효성 검사 어트리뷰트의 형태로 추상화시켰습니다. 이런 어트리뷰트들이 유효성 검사를 수행하는 코드를 담고 있기 때문에 개발자가 직접 작성해야만 하는 코드의 양이 줄어들었습니다.

유효성 검사 어트리뷰트

유효성 검사 어트리뷰트는 모델의 유효성 검사를 구성할 수 있는 방법 중 하나로, 개념적으로는 데이터베이스의 테이블 필드를 대상으로 한 유효성 검사 방식과 비슷합니다. 여기에는 데이터 형식 지정이나 필수 필드 같은 제약 조건들이 포함되어 있으며, 신용카드, 전화번호, 이메일 주소 같은 업무 규칙을 강제하는 데이터 패턴 적용 같은 그 밖의 다른 유형의 유효성 검사들도 지원됩니다. 유효성 검사 어트리뷰트를 사용하면 이런 요구사항들을 보다 간단하고 손쉽게 적용할 수 있습니다.

다음 코드는 영화나 TV 쇼의 정보를 관리하는 응용 프로그램에서 사용되는, 유효성 검사 어트리뷰트가 적용된 Movie 모델의 클래스 코드를 보여줍니다. 대부분의 속성들이 필수 입력으로 지정되어 있으며 일부 문자열 속성에는 길이 제한도 설정되어 있습니다. Price 속성에는 0부터 $999.99까지의 숫자 범위 제한이 지정되어 있으며, ReleaseDate 속성에는 사용자 지정 유효성 검사 어트리뷰트가 지정되어 있습니다.

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

    [Required]
    [StringLength(100)]
    public string Title { get; set; }

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

    [Required]
    [StringLength(1000)]
    public string Description { get; set; }

    [Required]
    [Range(0, 999.99)]
    public decimal Price { get; set; }

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

    public bool Preorder { get; set; }
}

단지 모델의 코드만 읽어봐도 간단하게 데이터에 대한 응용 프로그램의 규칙들을 파악할 수 있으며, 코드의 유지보수도 매우 쉽습니다. 다음은 자주 사용되는 몇 가지 기본적인 유효성 검사 어트리뷰트들입니다:

  • [CreditCard]: 속성 값이 신용카드 양식을 만족하는지 검사합니다.
  • [Compare]: 모델의 두 속성 값이 일치하는지 검사합니다.
  • [EmailAddress]: 속성 값이 이메일 양식을 만족하는지 검사합니다.
  • [Phone]: 속성 값이 전화번호 양식을 만족하는지 검사합니다.
  • [Range]: 속성 값이 지정된 범위 안에 포함되는지 검사합니다.
  • [RegularExpression]: 속성의 데이터가 지정한 정규 표현식과 일치하는지 검사합니다.
  • [Required]: 속성을 필수 입력으로 지정합니다.
  • [StringLength]: 문자열 속성이 지정된 최대 길이를 넘지 않는지 검사합니다.
  • [Url]: 속성 값이 URL 양식을 만족하는지 검사합니다.

MVC에서는 ValidationAttribute 클래스에서 파생된 모든 어트리뷰트를 유효성 검사의 용도로 사용할 수 있습니다. 다양한 유용한 유효성 검사 어트리뷰트들을 System.ComponentModel.DataAnnotations 네임스페이스에서 찾아볼 수 있습니다.

때로는 MVC에서 기본으로 제공되는 유효성 검사 어트리뷰트들이 지원해주는 것 이상의 기능이 필요한 경우도 있을 수 있습니다. 그런 경우에는 ValidationAttribute 클래스를 상속 받아서 사용자 지정 유효성 검사 어트리뷰트를 구현하거나, IValidatableObject 인터페이스를 구현하도록 모델 자체를 변경할 수 있습니다.

모델 상태

모델 상태는 제출된 HTML 폼 값의 유효성 검사 오류 여부를 나타냅니다.

MVC는 발생한 오류가 최대값에 도달할 때까지 필드들의 유효성 검사를 지속적으로 수행합니다 (기본 200개). 이 최대값은 다음과 같은 코드를 Startup.cs 파일의 ConfigureServices 메서드에 추가해서 설정할 수 있습니다:

services.AddMvc(options => options.MaxModelValidationErrors = 50);

모델 상태 오류 처리하기

모델의 유효성 검사는 컨트롤러 액션이 호출되기 전에 수행되며, ModelState.IsValid 속성을 확인하고 그 결과에 따라 적절하게 대응하는 작업은 액션 메서드의 역할입니다. 대부분의 경우 적절한 대응 방법은 모델 유효성 검사가 실패한 원인을 개념적으로 상세하게 기술한, 임의의 오류 응답을 반환하는 것입니다.

응용 프로그램에 따라서 모델 유효성 검사 오류에 대한 표준 처리 규칙을 기반으로 대응 방법을 자동으로 선택하도록 구현할 수도 있으며, 그런 경우 필터가 해당 정책을 작성하기에 적절한 장소가 될 수 있습니다. 이 경우, 모델 상태가 유효할 때와 유효하지 않을 때 액션이 각각 어떻게 동작하는지 테스트가 필요합니다.

수동 유효성 검사

이미 모델 바인딩과 유효성 검사가 완료됐지만, 일부 작업을 다시 반복하고 싶은 경우도 있습니다. 가령, 정수를 입력해야 할 필드에 사용자가 텍스트를 입력했다거나, 모델의 특정 속성 값을 다시 계산해야 할 수도 있습니다.

만약 수동으로 유효성 검사를 수행해야 한다면, 다음과 같이 TryValidateModel 메서드를 호출하면 됩니다:

TryValidateModel(movie);

사용자 지정 유효성 검사

내장 유효성 검사 어트리뷰트만으로도 대부분의 유효성 검사를 수행할 수 있습니다. 그러나 일부 유효성 검사 규칙은 특정 업무에 특화되어 있어서, 필수 입력 필드로 지정한다거나 값의 범위를 만족하는지를 확인하는 등과 같은 일반적인 데이터 유효성 검사만으로는 부족한 경우가 있습니다. 이런 시나리오에서는 사용자 지정 유효성 검사 어트리뷰트가 좋은 해결 방법입니다. MVC에서 사용자 지정 유효성 검사 어트리뷰트를 만드는 방법은 매우 간단합니다. 단지 ValidationAttribute 클래스를 상속 받아서 IsValid 메서드를 재정의하기만 하면 됩니다. IsValid 메서드는 두 가지 매개변수를 전달받는데, 그 중 첫 번째 매개변수는 value 라는 이름의 개체이고, 두 번째 매개변수는 validationContext 라는 이름의 ValidationContext 개체입니다. 여기서 value 는 사용자 지정 유효성 검사 어트리뷰트가 유효성을 검사할 필드의 실제 값을 참조합니다.

다음 예제는 사용자가 특정 년도 이후에 발표된 영화의 장르를 Classic 으로 설정할 수 없도록 제한하는 업무 규칙을 구현하고 있습니다. 이 [ClassicMovie] 어트리뷰트는 먼저 장르를 확인하는데, 만약 장르가 Classic 이라면 이번에는 발표일자가 특정 년도 이후인지를 확인합니다. 만약 영화가 특정 년도 이후에 발표됐다면 유효성 검사에 실패하게 됩니다. 이 어트리뷰트는 데이터 유효성 검사에 사용할 특정 년도를 의미하는 정수형 매개변수를 입력받으며, 이 매개변수 값은 다음과 같이 어트리뷰트의 생성자를 통해서 가져올 수 있습니다:

public class ClassicMovieAttribute : ValidationAttribute, IClientModelValidator
{
    private int _year;

    public ClassicMovieAttribute(int Year)
    {
        _year = Year;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        Movie movie = (Movie)validationContext.ObjectInstance;

        if (movie.Genre == Genre.Classic && movie.ReleaseDate.Year > _year)
        {
            return new ValidationResult(GetErrorMessage());
        }

        return ValidationResult.Success;
    }

이 코드에서 movie 변수는 유효성 검사의 대상인 제출된 데이터가 담겨 있는 Movie 개체를 참조하고 있습니다. 이 예제의 유효성 검사 코드는 ClassicMovieAttribute 클래스의 IsValid 메서드 내에서 규칙에 따라 발표일자와 장르를 확인합니다. IsValid 메서드는 유효성 검사에 성공할 경우 ValidationResult.Success 코드를 반환하고, 실패하면 오류 메시지와 함께 ValidationResult 개체를 반환합니다. 사용자가 Genre 필드를 수정하고 폼을 제출하면 ClassicMovieAttributeIsValid 메서드가 해당 영화의 Classic 여부를 확인합니다. 첫 번째 예제 코드에서 살펴본 것처럼 ClassicMovieAttribute도 다른 내장 어트리뷰트들과 마찬가지로 ReleaseDate 같은 속성에 적용하는 방식으로 유효성 검사를 수행합니다. 그러나 이번 예제는 Movie 형식을 대상으로만 동작하기 때문에 다음 문단에서 살펴볼 것처럼 IValidatableObject를 사용하는 방식이 더 바람직합니다.

모델에서 IValidatableObject 인터페이스의 Validate 메서드를 구현해서 동일한 코드를 모델 내부에 작성할 수도 있습니다. 사용자 지정 유효성 검사 어트리뷰트는 개별 속성들에 대한 유효성 검사에 적합한 반면, IValidatableObject 인터페이스 구현 방식은 다음과 같이 클래스 수준의 유효성 검사를 구현하는 데 유용하게 사용될 수 있습니다.

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    if (Genre == Genre.Classic && ReleaseDate.Year > _classicYear)
    {
        yield return new ValidationResult(
            "Classic movies must have a release year earlier than " + _classicYear,
            new[] { "ReleaseDate" });
    }
}

클라이언트 측 유효성 검사

클라이언트 측 유효성 검사는 사용자에게 많은 편리함을 제공해줍니다. 클라이언트 측 유효성 검사를 사용하지 않을 경우, 데이터가 서버로 전송되어 유효성 검사가 수행되고 그 결과를 응답받을 때까지 라운드 트립 시간 동안 대기해야만 합니다. 업무적인 측면에서 볼 때, 비록 몇 분의 1초에 불과한 시간이지만 날마다 수 백번 반복되면 결과적으로 많은 시간과 비용이 낭비되고, 불만들이 쌓이게 됩니다. 즉각적으로 직접적인 유효성 검사를 수행할 수 있다면 사용자가 보다 효율적으로 작업할 수 있으며, 더 나은 품질의 입력 및 출력을 만들어낼 수 있습니다.

클라이언트 측 유효성 검사가 정상적으로 동작하려면 다음과 같이 뷰에 적절한 JavaScript 스크립트 참조를 추가해야 합니다.

<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.3.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"></script>

MVC는 모델 속성에 대한 형식 메타데이터 뿐만 아니라 유효성 검사 어트리뷰트까지 모두 활용해서 데이터의 유효성을 검사하고 그 오류 메시지를 JavaScript로 출력합니다. 가령 다음과 같이 MVC에서 태그 헬퍼HTML 헬퍼로 모델을 폼 요소로 렌더하면, 클라이언트 측 유효성 검사에 사용되는 HTML 5의 data-어트리뷰트들이 폼 요소에 추가됩니다. MVC는 내장 어트리뷰트와 사용자 지정 어트리뷰트 모두에 대한 data- 어트리뷰트를 생성해줍니다. 클라이언트 측에서 유효성 검사 오류 메시지를 출력하려면 다음과 같이 관련된 태그 헬퍼를 사용하면 됩니다:

<div class="form-group">
    <label asp-for="ReleaseDate" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="ReleaseDate" class="form-control" />
        <span asp-validation-for="ReleaseDate" class="text-danger"></span>
    </div>
</div>

이 태그 헬퍼는 다음과 같은 HTML을 렌더하게 됩니다. 이 HTML 출력에 추가된 data- 어트리뷰트들과 ReleaseDate 속성에 적용된 유효성 검사 어트리뷰트들이 서로 대응된다는 점에 유의하시기 바랍니다. 가령 data-val-required 어트리뷰트에는 사용자가 발표일자를 입력하지 않았을 경우에 출력될 오류 메시지가 담겨 있으며, 이 메시지는 바로 뒤이은 <span> 요소에 출력됩니다.

<form action="/movies/Create" method="post">
  <div class="form-horizontal">
    <h4>Movie</h4>
    <div class="text-danger"></div>
    <div class="form-group">
      <label class="col-md-2 control-label" for="ReleaseDate">ReleaseDate</label>
      <div class="col-md-10">
        <input class="form-control" type="datetime"
          data-val="true" data-val-required="The ReleaseDate field is required."
          id="ReleaseDate" name="ReleaseDate" value="" />
        <span class="text-danger field-validation-valid"
          data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
      </div>
    </div>
  </div>
</form>

클라이언트 측 유효성 검사는 폼이 유효하지 않으면 제출 자체를 허용하지 않습니다. 제출 버튼을 누르면 폼을 제출하거나 오류 메시지를 출력하는 JavaScript가 실행됩니다.

MVC는 속성의 .NET 데이터 형식을 기반으로 폼 요소의 type 어트리뷰트 값을 결정하는데, 대부분 이 값은 [DataType] 어트리뷰트를 통해서 재지정되는 경우가 많습니다. 기본 [DataType] 어트리뷰트는 실질적인 서버 측 유효성 검사를 수행하지 않습니다. 브라우저가 자체적으로 필요하다고 판단되는 오류 메시지를 직접 선택하고 출력하는데, jQuery Validation Unobtrusive 패키지를 이용해서 다른 경우와 동일하게 일관된 방식으로 오류 메시지를 재정의하고 출력할 수 있습니다. 가령 [EmailAddress] 같은 [DataType]의 서브클래스를 적용한 경우, 이를 명확하게 확인할 수 있습니다.

IClientModelValidator

개발자가 직접 작성한 사용자 지정 유효성 검사 어트리뷰트에 대한 클라이언트 로직을 구현할 수도 있습니다. 그러면 클라이언트 측에서 튀지 않는 유효성 검사(Unobtrusive Validation)가 전체적인 유효성 검사의 한 과정으로 이를 실행시켜줍니다. 그 첫 번째 단계는 다음과 같이 IClientModelValidator 인터페이스를 구현해서 추가할 data- 어트리뷰트를 제어하는 것입니다:

public void AddValidation(ClientModelValidationContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    MergeAttribute(context.Attributes, "data-val", "true");
    MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

    var year = _year.ToString(CultureInfo.InvariantCulture);
    MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
}

유효성 검사 어트리뷰트에서 이 인터페이스를 구현하면 생성되는 필드에 HTML 어트리뷰트들을 추가할 수 있습니다. ReleaseDate 요소에 대한 HTML 출력을 살펴보면 이전 절의 예제와 그 결과가 상당히 비슷하다는 것을 알 수 있는데, 다만 IClientModelValidator 인터페이스의 AddValidation 메서드에서 정의한 data-val-classicmovie 관련 어트리뷰트들이 더 추가되어 있음을 확인할 수 있습니다.

<input class="form-control" type="datetime"
    data-val="true"
    data-val-classicmovie="Classic movies must have a release year earlier than 1960"
    data-val-classicmovie-year="1960"
    data-val-required="The ReleaseDate field is required."
    id="ReleaseDate" name="ReleaseDate" value="" />

튀지 않는 유효성 검사는 data- 어트리뷰트의 데이터를 이용해서 오류 메시지를 출력합니다. 그러나 jQuery의 validator 개체에 규칙과 메시지를 추가해주기 전까지는 jQuery가 해당 규칙이나 메시지의 존재를 알 수 없습니다. 다음 예제 코드는 jQuery의 validator 개체에 사용자 지정 클라이언트 측 유효성 검사 코드를 담고 있는 classicmovie라는 메서드를 추가하는 사례를 보여줍니다.

$(function () {
    jQuery.validator.addMethod('classicmovie',
        function (value, element, params) {
            // Get element value. Classic genre has value '0'.
            var genre = $(params[0]).val(),
                year = params[1],
                date = new Date(value);
            if (genre && genre.length > 0 && genre[0] === '0') {
                // Since this is a classic movie, invalid if release date is after given year.
                return date.getFullYear() <= year;
            }

            return true;
        });

    jQuery.validator.unobtrusive.adapters.add('classicmovie',
        [ 'element', 'year' ],
        function (options) {
            var element = $(options.form).find('select#Genre')[0];
            options.rules['classicmovie'] = [element, parseInt(options.params['year'])];
            options.messages['classicmovie'] = options.message;
        });
}(jQuery));

이 작업을 마치고 나면, jQuery가 사용자 지정 JavaScript 유효성 검사를 수행하기 위해서 필요한 정보와 해당 유효성 검사 코드가 false를 반환했을 때 출력해야할 메시지에 대한 정보를 알 수 있습니다.

원격 유효성 검사

원격 유효성 검사는 클라이언트에서 서버의 데이터를 대상으로 데이터의 유효성을 검사해야 할 때 활용할 수 있는 매우 유용한 기능입니다. 가령, 응용 프로그램에서 이메일 주소나 사용자 이름이 이미 사용 중인지 여부를 확인해야 할 경우, 이 작업을 처리하려면 대량의 데이터를 조회해봐야 합니다. 그러나 불과 한 두 가지 필드의 유효성을 검사하기 위해서 대량의 데이터 집합을 다운로드 하는 것은 너무 많은 리소스가 낭비됩니다. 게다가 민감한 정보가 노출될 수도 있습니다. 이에 대한 대안은 필드의 유효성을 검사하는 라운드 트립 요청을 전송하는 것입니다.

원격 유효성 검사는 두 단계의 과정을 통해서 구현할 수 있습니다. 먼저 [Remote] 어트리뷰트를 모델에 적용해야 합니다. [Remote] 어트리뷰트에는 클라이언트 측의 JavaScript가 적절한 코드를 호출하도록 설정할 수 있는 다양한 오버로드 버전이 존재합니다. 가령 다음 예제에서는 Users 컨트롤러의 VerifyEmail 액션 메서드를 지정하고 있습니다.

public class User
{
    [Remote(action: "VerifyEmail", controller: "Users")]
    public string Email { get; set; }
}

두 번째 단계는 [Remote] 어트리뷰트에 지정된 액션 메서드에 유효성 검사 코드를 구현하는 것입니다. 이 메서드는 클라이언트 측에서 작업을 계속 진행하거나 중단할지를 판단하거나, 필요한 경우 오류 메시지를 출력할 때 사용할 수 있는 JsonResult를 반환합니다.

[AcceptVerbs("Get", "Post")]
public IActionResult VerifyEmail(string email)
{
    if (!_userRepository.VerifyEmail(email))
    {
        return Json(data: $"Email {email} is already in use.");
    }

    return Json(data: true);
}

이제 사용자가 이메일을 입력하면 뷰의 JavaScript가 원격 호출을 수행해서 입력된 이메일 주소가 이미 사용 중인지 여부를 확인합니다. 만약 이미 사용 중이라면 오류 메시지가 출력되고, 아니라면 정상적으로 폼이 제출될 것입니다.