6 min read

FluentValidation 라이브러리를 이용한 ASP.NET MVC 모델 유효성 검사

Justin Yoo

ASP.NET MVC 웹 애플리케이션에서 사용자 입력값의 유효성을 검사하는 방법은 여러가지가 있겠지만, 보통은 DataAnnodations 방법을 이용한다. 그러나, 여기 소개하는 FluentValidation 라이브러리를 이용하면 훨씬 더 편리하게 유효성 검사를 수행할 수 있다. 이 포스트에서는 이 FluentValidation 라이브러리를 소개하고, 이를 ASP.NET MVC 웹 애플리케이션과 Web API 애플리케이션에서 사용하는 방법, 유닛 테스트를 수행하는 방법, 그리고 IoC 콘트롤러에서 활용하는 방법에 대해 간단하게 논의해 보도록 하자. 여기에 쓰인 코드는 아래 리포지토리에서 확인할 수 있다.

  1. FluentValidation 라이브러리를 이용한 ASP.NET MVC 모델 유효성 검사
  2. FluentValidation 라이브러리 유닛 테스트
  3. FluentValidation 제어 역전 혹은 의존성 주입 설정

전형적인 사용자 입력 유효성 검사

일반적으로 ASP.NET MVC 애플리케이션에서 사용자 입력에 대한 유효성 검사를 위해서는 바인딩 모델에 DataAnnotation 을 아래와 같은 식으로 추가한다.

public class RegisterViewModel
{
  [Required]
  [DataType(DataType.EmailAddress)]
  [Display(Name = "Email")]
  public string Email { get; set; }

  [Required]
  [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password { get; set; }

  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
  public string ConfirmPassword { get; set; }
}

위에서 확인할 수 있다 시피 RequiredStringLength 같은 속성클라스를 이용하여 해당 모델의 유효성 검사를 진행한다. 이를 이용하면 콘트롤러에서는 보통 이런 모양이 될 수 있다.

[HttpPost]
public virtual async Task<ActionResult> Register(RegisterViewModel form)
{
  var vm = form;
  if (ModelState.IsValid)
  {
    vm.Validated = true;
  }
  return View(vm);
}

이렇게 하면 ModelState.IsValid을 확인하는 시점에서 이미 유효성 검사는 끝났다고 볼 수 있다. 이렇게 해도 아무런 문제가 없긴 하다만, 모델 바인딩 클라스를 만들면서 굉장히 번거로운 작업이 필요하다. 이 과정을 획기적으로 줄여줄 수 있는 것이 바로 FluentValidation library이다.

FluentValidation 라이브러리로 변환

이제 FluentValidation 라이브러리를 설치해서 사용해 보도록 하자. NuGet 패키지를 다운로드 받아 설치한 후 아래와 같이 코드를 바꾸어 보자.

[Validator(typeof(RegisterViewModelValidator))]
public class RegisterViewModel
{
  [Display(Name = "Email")]
  public string Email { get; set; }

  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password { get; set; }

  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  public string ConfirmPassword { get; set; }
}

위에서 보는 바와 같이 바인딩 모델은 아주 간단하게 바뀌었다. 대신 Validator라는 속성 클라스를 추가한 것을 확인할 수 있는데, 이 속성 클라스의 정의는 아래와 같다.

public class RegisterViewModelValidator : AbstractValidator<RegisterViewModel>
{
  public RegisterViewModelValidator()
  {
    RuleFor(x => x.Email)
      .NotNull().WithMessage("Required")
      .EmailAddress().WithMessage("Invalid email");

    RuleFor(x => x.Password)
      .NotNull().WithMessage("Required")
      .Length(6, 100).WithMessage("Too short or too long");

    RuleFor(x => x.ConfirmPassword)
      .NotNull().WithMessage("Required")
      .Equal(x => x.Password).WithMessage("Not matched");
  }
}

이렇게 Validator 정의를 별도의 클라스에 한꺼번에 몰아서 선언하고 이를 Global.asax.csApplication_Start() 메소드에서 초기화한다.

public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    ...

    FluentValidationModelValidatorProvider.Configure();
  }
}

이렇게 한 후 다시 콘트롤러에서 평소와 같이 ModelState.IsValid 속성을 호출하면 된다.

Razor 뷰

그렇다면, 이에 대응하는 Razor 뷰는 어떻게 작성할까? 대략 아래와 같은 모습이 될 것이다.

@using (Html.BeginForm(MVC.Home.ActionNames.Register, MVC.Home.Name, FormMethod.Post))
{
  <div>
    @Html.LabelFor(m => m.Email)
    <div>
      @Html.TextBoxFor(m => m.Email, new Dictionary<string, object>() { { "placeholder", "Email" } })
      @Html.ValidationMessageFor(m => m.Email)
    </div>
  </div>
  <div>
    @Html.LabelFor(m => m.Password)
    <div>
      @Html.PasswordFor(m => m.Password, new Dictionary<string, object>() { { "placeholder", "Password" } })
      @Html.ValidationMessageFor(m => m.Password)
    </div>
  </div>
  <div>
    @Html.LabelFor(m => m.ConfirmPassword)
    <div>
      @Html.PasswordFor(m => m.ConfirmPassword, new Dictionary<string, object>() { { "placeholder", "Confirm Password" } })
      @Html.ValidationMessageFor(m => m.ConfirmPassword)
    </div>
  </div>
  <div>
    <div>
      <input type="submit" name="Submit" />
    </div>
  </div>
}

이후 실제로 웹사이트를 구동시킨 후 렌더링 된 HTML 코드는 아래와 같다.

<form action="/Home/Register" method="post">
  <div>
    <label for="Email">Email</label>
    <div>
      <input data-val="true" data-val-email="Invalid email" data-val-required="Required" id="Email" name="Email" placeholder="Email" type="text" value="" />
      <span class="field-validation-valid" data-valmsg-for="Email" data-valmsg-replace="true"/>
    </div>
  </div>
  <div>
    <label for="Password">Password</label>
    <div>
      <input data-val="true" data-val-length="Too short or too long" data-val-length-max="100" data-val-length-min="6" data-val-required="Required" id="Password" name="Password" placeholder="Password" type="password" />
      <span class="field-validation-valid" data-valmsg-for="Password" data-valmsg-replace="true"/>
    </div>
  </div>
  <div>
    <label for="ConfirmPassword">Confirm password</label>
    <div>
      <input data-val="true" data-val-equalto="Not matched" data-val-equalto-other="*.Password" data-val-required="Required" id="ConfirmPassword" name="ConfirmPassword" placeholder="Confirm Password" type="password" />
      <span class="field-validation-valid" data-valmsg-for="ConfirmPassword" data-valmsg-replace="true"/>
    </div>
  </div>
  <div>
    <div>
      <input type="submit" name="Submit" />
    </div>
  </div>
</form>

이렇게 해서 FluentValidation 라이브러리로 유효성 검사를 대체하는 방법에 대해 알아보았다. 그렇다면 이러한 유효성 검사에 대한 것들을 어떻게 테스트를 할 수 있을까? 다음 포스트에서 알아보도록 하자. 60초 후에 돌아오겠습니다