다음을 통해 공유


서비스 레이어를 사용한 유효성 검사(C#)

작성 자: Stephen Walther

컨트롤러 작업에서 별도의 서비스 계층으로 유효성 검사 논리를 이동하는 방법을 알아봅니다. 이 자습서에서 Stephen Walther는 컨트롤러 계층에서 서비스 계층을 격리하여 문제를 급격히 분리하는 방법을 설명합니다.

이 자습서의 목표는 ASP.NET MVC 애플리케이션에서 유효성 검사를 수행하는 한 가지 방법을 설명하는 것입니다. 이 자습서에서는 컨트롤러에서 별도의 서비스 계층으로 유효성 검사 논리를 이동하는 방법을 알아봅니다.

우려 사항 분리

ASP.NET MVC 애플리케이션을 빌드할 때 컨트롤러 작업 내에 데이터베이스 논리를 배치하면 안 됩니다. 데이터베이스와 컨트롤러 논리를 혼합하면 시간이 지남에 따라 애플리케이션을 유지 관리하기가 더 어려워집니다. 모든 데이터베이스 논리를 별도의 리포지토리 계층에 배치하는 것이 좋습니다.

예를 들어 목록 1에는 ProductRepository라는 간단한 리포지토리가 포함되어 있습니다. 제품 리포지토리에는 애플리케이션에 대한 모든 데이터 액세스 코드가 포함되어 있습니다. 목록에는 제품 리포지토리에서 구현하는 IProductRepository 인터페이스도 포함됩니다.

목록 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() 작업 모두에서 리포지토리 계층을 사용합니다. 이 컨트롤러에는 데이터베이스 논리가 포함되어 있지 않습니다. 리포지토리 계층을 만들면 클린 문제 분리를 유지할 수 있습니다. 컨트롤러는 애플리케이션 흐름 제어 논리를 담당하며 리포지토리는 데이터 액세스 논리를 담당합니다.

목록 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");
        }

    }
}

서비스 계층 만들기

따라서 애플리케이션 흐름 제어 논리는 컨트롤러에 속하고 데이터 액세스 논리는 리포지토리에 속합니다. 이 경우 유효성 검사 논리는 어디에 배치합니까? 한 가지 옵션은 유효성 검사 논리를 서비스 계층에 배치하는 것입니다.

서비스 계층은 컨트롤러와 리포지토리 계층 간의 통신을 중재하는 ASP.NET MVC 애플리케이션의 추가 계층입니다. 서비스 계층에는 비즈니스 논리가 포함됩니다. 특히 유효성 검사 논리가 포함되어 있습니다.

예를 들어 목록 3의 제품 서비스 계층에는 CreateProduct() 메서드가 있습니다. CreateProduct() 메서드는 제품 리포지토리에 제품을 전달하기 전에 ValidateProduct() 메서드를 호출하여 새 제품의 유효성을 검사합니다.

목록 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에서 업데이트되었습니다. 컨트롤러 계층은 서비스 계층과 대화합니다. 서비스 계층은 리포지토리 계층과 대화합니다. 각 계층에는 별도의 책임이 있습니다.

목록 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");
        }

    }
}

제품 서비스는 제품 컨트롤러 생성자에서 만들어집니다. 제품 서비스를 만들면 모델 상태 사전이 서비스에 전달됩니다. 제품 서비스는 모델 상태를 사용하여 유효성 검사 오류 메시지를 컨트롤러에 다시 전달합니다.

서비스 계층 분리

컨트롤러와 서비스 계층을 한 가지 측면에서 격리하지 못했습니다. 컨트롤러 및 서비스 계층은 모델 상태를 통해 통신합니다. 즉, 서비스 계층은 ASP.NET MVC 프레임워크의 특정 기능에 종속됩니다.

가능한 한 컨트롤러 계층에서 서비스 계층을 격리하려고 합니다. 이론적으로는 ASP.NET MVC 애플리케이션뿐만 아니라 모든 유형의 애플리케이션에서 서비스 계층을 사용할 수 있어야 합니다. 예를 들어 나중에 애플리케이션에 대한 WPF 프런트 엔드를 빌드할 수 있습니다. 서비스 계층에서 ASP.NET MVC 모델 상태에 대한 종속성을 제거하는 방법을 찾아야 합니다.

목록 5에서 서비스 계층이 더 이상 모델 상태를 사용하지 않도록 업데이트되었습니다. 대신 IValidationDictionary 인터페이스를 구현하는 모든 클래스를 사용합니다.

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

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에 정의되어 있습니다. 이 간단한 인터페이스에는 단일 메서드와 단일 속성이 있습니다.

목록 6 - Models\IValidationDictionary.cs

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

ModelStateWrapper 클래스라는 목록 7의 클래스는 IValidationDictionary 인터페이스를 구현합니다. 모델 상태 사전을 생성자에 전달하여 ModelStateWrapper 클래스를 인스턴스화할 수 있습니다.

목록 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를 사용합니다.

목록 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 클래스를 사용하면 서비스 계층을 컨트롤러 계층에서 완전히 격리할 수 있습니다. 서비스 계층은 더 이상 모델 상태에 종속되지 않습니다. IValidationDictionary 인터페이스를 구현하는 모든 클래스를 서비스 계층에 전달할 수 있습니다. 예를 들어 WPF 애플리케이션은 간단한 컬렉션 클래스를 사용하여 IValidationDictionary 인터페이스를 구현할 수 있습니다.

요약

이 자습서의 목표는 ASP.NET MVC 애플리케이션에서 유효성 검사를 수행하는 한 가지 방법을 설명하는 것이었습니다. 이 자습서에서는 모든 유효성 검사 논리를 컨트롤러에서 별도의 서비스 계층으로 이동하는 방법을 알아보았습니다. 또한 ModelStateWrapper 클래스를 만들어 컨트롤러 계층에서 서비스 계층을 격리하는 방법도 알아보았습니다.