서비스 레이어를 사용한 유효성 검사(C#)Validating with a Service Layer (C#)

Stephen Waltherby Stephen Walther

컨트롤러 작업 및 별도의 서비스 계층으로 유효성 검사 논리를 이동 하는 방법에 대해 알아봅니다.Learn how to move your validation logic out of your controller actions and into a separate service layer. 이 자습서에서 Stephen Walther는 컨트롤러 계층에서 서비스 계층을 격리 하 여 문제를 선명 하 게 분리 하는 방법을 설명 합니다.In this tutorial, Stephen Walther explains how you can maintain a sharp separation of concerns by isolating your service layer from your controller layer.

이 자습서의 목표는 ASP.NET MVC 응용 프로그램에서 유효성 검사를 수행 하는 한 가지 방법을 설명 하는 것입니다.The goal of this tutorial is to describe one method of performing validation in an ASP.NET MVC application. 이 자습서에서는 컨트롤러의 유효성 검사 논리를 별도의 서비스 계층으로 이동 하는 방법에 대해 알아봅니다.In this tutorial, you learn how to move your validation logic out of your controllers and into a separate service layer.

문제 구분Separating Concerns

ASP.NET MVC 응용 프로그램을 빌드하는 경우에는 데이터베이스 논리를 컨트롤러 작업 안에 넣지 않아야 합니다.When you build an ASP.NET MVC application, you should not place your database logic inside your controller actions. 데이터베이스와 컨트롤러 논리를 함께 사용 하면 응용 프로그램을 시간이 지남에 따라 유지 하기가 더 어려워집니다.Mixing your database and controller logic makes your application more difficult to maintain over time. 모든 데이터베이스 논리를 별도의 리포지토리 계층에 저장 하는 것이 좋습니다.The recommendation is that you place all of your database logic in a separate repository layer.

예를 들어 목록 1은 ProductRepository 라는 간단한 리포지토리를 포함 합니다.For example, Listing 1 contains a simple repository named the ProductRepository. 제품 리포지토리에는 응용 프로그램에 대 한 모든 데이터 액세스 코드가 포함 됩니다.The product repository contains all of the data access code for the application. 또한 목록에는 제품 리포지토리에서 구현 하는 IProductRepository 인터페이스가 포함 됩니다.The listing also includes the IProductRepository interface that the product repository implements.

목록 1--Models\ProductRepository.csListing 1 -- Models\ProductRepository.cs

using System.Collections.Generic;
using System.Linq;

namespace MvcApplication1.Models
{
    public class ProductRepository : MvcApplication1.Models.IProductRepository
    {
        private ProductDBEntities _entities = new ProductDBEntities();

        public IEnumerable<Product> ListProducts()
        {
            return _entities.ProductSet.ToList();
        }

        public bool CreateProduct(Product productToCreate)
        {
            try
            {
                _entities.AddToProductSet(productToCreate);
                _entities.SaveChanges();
                return true;
            }
            catch
            {
                return false;
            }
        }

    }

    public interface IProductRepository
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }

}

목록 2의 컨트롤러는 해당 Index () 및 Create () 작업에서 리포지토리 계층을 사용 합니다.The controller in Listing 2 uses the repository layer in both its Index() and Create() actions. 이 컨트롤러는 데이터베이스 논리를 포함 하지 않습니다.Notice that this controller does not contain any database logic. 리포지토리 계층을 만들면 문제를 완전히 분리 하 여 유지할 수 있습니다.Creating a repository layer enables you to maintain a clean separation of concerns. 컨트롤러는 응용 프로그램 흐름 제어 논리를 담당 하며 리포지토리는 데이터 액세스 논리를 담당 합니다.Controllers are responsible for application flow control logic and the repository is responsible for data access logic.

목록 2-Controllers\ProductController.csListing 2 - Controllers\ProductController.cs

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductRepository _repository;

        public ProductController():
            this(new ProductRepository()) {}

        public ProductController(IProductRepository repository)
        {
            _repository = repository;
        }

        public ActionResult Index()
        {
            return View(_repository.ListProducts());
        }

        //
        // GET: /Product/Create

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

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude="Id")] Product productToCreate)
        {
            _repository.CreateProduct(productToCreate);
            return RedirectToAction("Index");
        }

    }
}

서비스 계층 만들기Creating a Service Layer

따라서 응용 프로그램 흐름 제어 논리는 컨트롤러에 속하고 데이터 액세스 논리는 리포지토리에 속합니다.So, application flow control logic belongs in a controller and data access logic belongs in a repository. 이 경우 유효성 검사 논리를 어디에 배치 해야 하나요?In that case, where do you put your validation logic? 한 가지 옵션은 서비스 계층에 유효성 검사 논리를 추가 하는 것입니다.One option is to place your validation logic in a service layer.

서비스 계층은 컨트롤러와 리포지토리 계층 간의 통신을 중재 하는 ASP.NET MVC 응용 프로그램의 추가 계층입니다.A service layer is an additional layer in an ASP.NET MVC application that mediates communication between a controller and repository layer. 서비스 계층에는 비즈니스 논리가 포함 되어 있습니다.The service layer contains business logic. 특히 유효성 검사 논리를 포함 합니다.In particular, it contains validation logic.

예를 들어 목록 3의 제품 서비스 계층에는 CreateProduct () 메서드가 있습니다.For example, the product service layer in Listing 3 has a CreateProduct() method. CreateProduct () 메서드는 product 리포지토리에 제품을 전달 하기 전에 ValidateProduct () 메서드를 호출 하 여 새 제품의 유효성을 검사 합니다.The CreateProduct() method calls the ValidateProduct() method to validate a new product before passing the product to the product repository.

목록 3-Models\ententservice.csListing 3 - Models\ProductService.cs

using System.Collections.Generic;
using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ProductService : IProductService
    {

        private ModelStateDictionary _modelState;
        private IProductRepository _repository;

        public ProductService(ModelStateDictionary modelState, IProductRepository repository)
        {
            _modelState = modelState;
            _repository = repository;
        }

        protected bool ValidateProduct(Product productToValidate)
        {
            if (productToValidate.Name.Trim().Length == 0)
                _modelState.AddModelError("Name", "Name is required.");
            if (productToValidate.Description.Trim().Length == 0)
                _modelState.AddModelError("Description", "Description is required.");
            if (productToValidate.UnitsInStock < 0)
                _modelState.AddModelError("UnitsInStock", "Units in stock cannot be less than zero.");
            return _modelState.IsValid;
        }

        public IEnumerable<Product> ListProducts()
        {
            return _repository.ListProducts();
        }

        public bool CreateProduct(Product productToCreate)
        {
            // Validation logic
            if (!ValidateProduct(productToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateProduct(productToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }

    public interface IProductService
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }
}

목록 4에서 제품 컨트롤러가 업데이트 되어 리포지토리 계층 대신 서비스 계층을 사용 합니다.The Product controller has been updated in Listing 4 to use the service layer instead of the repository layer. 컨트롤러 계층은 서비스 계층에 대해 통신 합니다.The controller layer talks to the service layer. 서비스 계층은 리포지토리 계층에 대해 통신 합니다.The service layer talks to the repository layer. 각 계층에는 별도의 책임이 있습니다.Each layer has a separate responsibility.

목록 4-Controllers\ProductController.csListing 4 - Controllers\ProductController.cs

Listing 4 – Controllers\ProductController.cs
using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductService _service;

        public ProductController() 
        {
            _service = new ProductService(this.ModelState, new ProductRepository());
        }

        public ProductController(IProductService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListProducts());
        }

        //
        // GET: /Product/Create

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

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Product productToCreate)
        {
            if (!_service.CreateProduct(productToCreate))
                return View();
            return RedirectToAction("Index");
        }

    }
}

Product service는 product controller 생성자에 생성 됩니다.Notice that the product service is created in the product controller constructor. 제품 서비스가 생성 되 면 모델 상태 사전이 서비스에 전달 됩니다.When the product service is created, the model state dictionary is passed to the service. 제품 서비스는 모델 상태를 사용 하 여 유효성 검사 오류 메시지를 컨트롤러에 다시 전달 합니다.The product service uses model state to pass validation error messages back to the controller.

서비스 계층 분리Decoupling the Service Layer

컨트롤러 및 서비스 계층을 하나의 관점에서 격리 하지 못했습니다.We have failed to isolate the controller and service layers in one respect. 컨트롤러 및 서비스 계층은 모델 상태를 통해 통신 합니다.The controller and service layers communicate through model state. 즉, 서비스 계층은 ASP.NET MVC 프레임 워크의 특정 기능에 종속 됩니다.In other words, the service layer has a dependency on a particular feature of the ASP.NET MVC framework.

가능 하면 컨트롤러 계층에서 서비스 계층을 격리 하려고 합니다.We want to isolate the service layer from our controller layer as much as possible. 이론적으로 ASP.NET MVC 응용 프로그램 뿐만 아니라 모든 유형의 응용 프로그램에서 서비스 계층을 사용할 수 있어야 합니다.In theory, we should be able to use the service layer with any type of application and not only an ASP.NET MVC application. 예를 들어 나중에 응용 프로그램에 대 한 WPF 프런트 엔드를 빌드할 수 있습니다.For example, in the future, we might want to build a WPF front-end for our application. 서비스 계층에서 ASP.NET MVC 모델 상태에 대 한 종속성을 제거 하는 방법을 찾아야 합니다.We should find a way to remove the dependency on ASP.NET MVC model state from our service layer.

목록 5에서 서비스 계층은 더 이상 모델 상태를 사용 하지 않도록 업데이트 되었습니다.In Listing 5, the service layer has been updated so that it no longer uses model state. 대신, IValidationDictionary 인터페이스를 구현 하는 클래스를 사용 합니다.Instead, it uses any class that implements the IValidationDictionary interface.

목록 5-Models\ProductService.cs (분리)Listing 5 - Models\ProductService.cs (decoupled)

using System.Collections.Generic;

namespace MvcApplication1.Models
{
    public class ProductService : IProductService
    {

        private IValidationDictionary _validatonDictionary;
        private IProductRepository _repository;

        public ProductService(IValidationDictionary validationDictionary, IProductRepository repository)
        {
            _validatonDictionary = validationDictionary;
            _repository = repository;
        }

        protected bool ValidateProduct(Product productToValidate)
        {
            if (productToValidate.Name.Trim().Length == 0)
                _validatonDictionary.AddError("Name", "Name is required.");
            if (productToValidate.Description.Trim().Length == 0)
                _validatonDictionary.AddError("Description", "Description is required.");
            if (productToValidate.UnitsInStock < 0)
                _validatonDictionary.AddError("UnitsInStock", "Units in stock cannot be less than zero.");
            return _validatonDictionary.IsValid;
        }

        public IEnumerable<Product> ListProducts()
        {
            return _repository.ListProducts();
        }

        public bool CreateProduct(Product productToCreate)
        {
            // Validation logic
            if (!ValidateProduct(productToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateProduct(productToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }

    public interface IProductService
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }
}

IValidationDictionary 인터페이스는 목록 6에 정의 되어 있습니다.The IValidationDictionary interface is defined in Listing 6. 이 간단한 인터페이스에는 단일 메서드와 단일 속성이 있습니다.This simple interface has a single method and a single property.

목록 6-Models\IValidationDictionary.csListing 6 - Models\IValidationDictionary.cs

namespace MvcApplication1.Models
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid { get; }
    }
}

ModelStateWrapper 클래스 라는 목록 7의 클래스는 IValidationDictionary 인터페이스를 구현 합니다.The class in Listing 7, named the ModelStateWrapper class, implements the IValidationDictionary interface. 모델 상태 사전을 생성자에 전달 하 여 ModelStateWrapper 클래스를 인스턴스화할 수 있습니다.You can instantiate the ModelStateWrapper class by passing a model state dictionary to the constructor.

목록 7-Models\ModelStateWrapper.csListing 7 - Models\ModelStateWrapper.cs

using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ModelStateWrapper : IValidationDictionary
    {

        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        #region IValidationDictionary Members

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }

        #endregion
    }
}

마지막으로 목록 8의 업데이트 된 컨트롤러는 해당 생성자에서 서비스 계층을 만들 때 ModelStateWrapper를 사용 합니다.Finally, the updated controller in Listing 8 uses the ModelStateWrapper when creating the service layer in its constructor.

8-Controller\stvv 제품 목록 표시Listing 8 - Controllers\ProductController.cs

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductService _service;

        public ProductController() 
        {
            _service = new ProductService(new ModelStateWrapper(this.ModelState), new ProductRepository());
        }

        public ProductController(IProductService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListProducts());
        }

        //
        // GET: /Product/Create

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

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Product productToCreate)
        {
            if (!_service.CreateProduct(productToCreate))
                return View();
            return RedirectToAction("Index");
        }

    }
}

IValidationDictionary 인터페이스 및 ModelStateWrapper 클래스를 사용 하 여 서비스 계층을 컨트롤러 계층에서 완전히 격리할 수 있습니다.Using the IValidationDictionary interface and the ModelStateWrapper class enables us to completely isolate our service layer from our controller layer. 서비스 계층은 더 이상 모델 상태에 종속 되지 않습니다.The service layer is no longer dependent on model state. IValidationDictionary 인터페이스를 구현 하는 모든 클래스를 서비스 계층에 전달할 수 있습니다.You can pass any class that implements the IValidationDictionary interface to the service layer. 예를 들어, WPF 응용 프로그램은 간단한 컬렉션 클래스를 사용 하 여 IValidationDictionary 인터페이스를 구현할 수 있습니다.For example, a WPF application might implement the IValidationDictionary interface with a simple collection class.

요약Summary

이 자습서의 목표는 ASP.NET MVC 응용 프로그램에서 유효성 검사를 수행 하는 방법에 대해 설명 하는 것입니다.The goal of this tutorial was to discuss one approach to performing validation in an ASP.NET MVC application. 이 자습서에서는 모든 유효성 검사 논리를 컨트롤러에서 별도의 서비스 계층으로 이동 하는 방법을 배웠습니다.In this tutorial, you learned how to move all of your validation logic out of your controllers and into a separate service layer. 또한 ModelStateWrapper 클래스를 만들어 컨트롤러 계층에서 서비스 계층을 분리 하는 방법을 배웠습니다.You also learned how to isolate your service layer from your controller layer by creating a ModelStateWrapper class.