ASP.NET MVC - Validation(3)



이번 시간은 컨트롤러에 있었던 유효성 검사 로직을 서비스 계층으로 옮기도록 하겠습니다. 
관점의 분리가 향후 유지보수에 날개를 달아주길 기대하며...

서비스 계층에 유효성 검사를

일단 기분좋게 웃으면서 시작하시죠. ㅎㅎㅎ

관점을 분리시키자

ASP.NET MVC 어플리케이션을 빌드하게 될때, 컨트롤러 액션들안에 데이터베이스 로직을 두는것은 그렇게 바람직하지는 않습니다. 데이터베이스와 컨트롤러 로직이 혼합되게되면, 향후 유지보수가 더 어려워질것이기 때문이죠.(아니라고 생각된다면, 이번 포스팅은 안보셔도 됩니다ㅡ.ㅡ)
음.. 추천되는 것은 데이터베이스 로직을 리파지터리 계층에 두는 것이죠.

예를들어, TelDirRepository라고 이름지은 간단한 리파지터리가 있습니다.  이 리파지터리는 어플리케이션에서 디비에 액세스하는 모든 코드를 포함하고 있습니다. 또 한가지 ITelDirRepository 인터페이스를 구현하고 있네요.

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

namespace Validation1.Models
{
    public class TelDirRepository : ITelDirRepository
    {
        private MvcDBEntities _entities = new MvcDBEntities();

        public bool CreateTelDir(TelDir telDirToCreate)
        {
            try
            {
                _entities.AddToTelDirSet(telDirToCreate);
                _entities.SaveChanges();
                return true;
            }
            catch
            {
                return false;
            }
        }

        public IEnumerable<TelDir> ListTelDir()
        {
            return _entities.TelDirSet.ToList();
        }
    }

    public interface ITelDirRepository
    {
        bool CreateTelDir(TelDir telDirToCreate);
        IEnumerable<TelDir> ListTelDir();
    }
}

컨트롤러의 액션메쏘드에서 이 리파지터리를 사용해보겠습니다. 이 컨트롤러에는 어떠한 데이터베이스 로직도 포함되어 있지 않습니다. 리파지터리 계층을 생성함으로써 깔끔한 관점의 분리를 할수 있는거죠. 컨트롤러는 어플리케이션의 흐름을 담당하고, 리파지터리는 데이터 액세스 로직을 담당하고. 좋죠? ^^;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Validation1.Models;

namespace Validation1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        private ITelDirRepository _repository;

        public HomeController():this(new TelDirRepository()) {}

        public HomeController(ITelDirRepository repository)
        {
            _repository = repository;
        }

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

        [HttpPost]
        public ActionResult Create([Bind(Exclude="Id")] TelDir dir)
        {
            _repository.CreateTelDir(dir);
            return RedirectToAction("Index");
        }
    }
}

서비스 계층을 만들자

자, 어플리케이션의 흐름의 컨트롤 로직은 컨트롤러에, 데이터 액세스 로직은 리파지터에 속해있습니다. 여기서 유효성 검사 로직은 어디에 포함되어야 할까요? 하나의 옵션을 주자면, 서비스 계층에 두는 것인거죠.(드디어 여기까지 왔네요;;)

서비스 계층은 ASP.NET MVC 어플리케이션에서 컨트롤러와 리파지터리 계층 사이의 커뮤니케이션을 위해 추가된 계층이라고 보시면 됩니다. 이 서비스 계층이 비즈니스 로직을 담당하게 되죠. 특히 유효성 검사로직을 포함하게 되죠.

예를들어, TelDir 서비스 계층에 CreateTelDir() 메소드가 있습니다. 이 CreateTelDir() 메쏘드는 새 TelDir 을 TelDir 리파지터리에 전달하기 전에 유효성 검사를 하는 ValidateTelDir() 메쏘드를 호출하죠.

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

namespace Validation1.Models
{
    public class TelDirService : ITelDirService
    {
        private ModelStateDictionary _modelState;
        private ITelDirRepository _repository;

        public TelDirService(ModelStateDictionary modelState, ITelDirRepository repository)
        {
            _modelState = modelState;
            _repository = repository;
        }

        protected bool ValidateTelDir(TelDir telDirToValidate)
        {
            if (string.IsNullOrEmpty(telDirToValidate.Name))
                _modelState.AddModelError("Name", "이름이 필요해.");
            if (string.IsNullOrEmpty(telDirToValidate.Phone))
                _modelState.AddModelError("Phone", "전화번호가 필요해.");
            if (telDirToValidate.SpeedDial.HasValue)
            {
                if (telDirToValidate.SpeedDial < 1 || telDirToValidate.SpeedDial > 99)
                    _modelState.AddModelError("SpeedDial", "단축다이얼 다시 입력해.(1~99)");
            }
            return _modelState.IsValid;
        }

        public bool CreateTelDir(TelDir telDirToCreate)
        {
            if(!ValidateTelDir(telDirToCreate))
                return false;
            try
            {
                _repository.CreateTelDir(telDirToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public IEnumerable<TelDir> ListTelDir()
        {
            return _repository.ListTelDir();
        }
    }

    public interface ITelDirService
    {
        bool CreateTelDir(TelDir telDirToCreate);
        IEnumerable<TelDir> ListTelDir();
    }
}

다시 Home 컨트롤러를 수정하겠습니다. 리파지터리 계층 대신 서비스 계층을 이용하도록 말이죠. 컨트롤러 계층이 서비스 계층을 부르고, 서비스 계층은 리파지터리 계층을 부르니, 이야말로 계층의 분리, 각 계층이 담당할 것이 분리된 셈이죠.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Validation1.Models;

namespace Validation1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        private ITelDirService _service;

        public HomeController()
        {
            _service = new TelDirService(this.ModelState, new TelDirRepository());
        }

        public HomeController(ITelDirService service)
        {
            _service = service;
        }

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

        [HttpPost]
        public ActionResult Create([Bind(Exclude="Id")] TelDir dir)
        {
            if (!_service.CreateTelDir(dir))
                return View();
            return RedirectToAction("Index");
        }
    }
}

TelDir 서비스는 Home 컨트롤러의 생성자에서 생성이 되었습니다. 서비스가 생성될때 모델 스테이트 딕셔너리를 서비스에 전달합니다. 다시 서비스는 그 모델 스테이트를 이용해서 유효성 에러들을 컨트롤러에게 전달합니다.

서비스 계층을 다시 분리하자

음. 한가지 관점에서 보게되면 컨트롤러와 서비스 계층을 독립적으로 생각한다는것이 무리인듯 싶네요. 왜냐?
바로 모델 스테이트 때문이죠. 컨트롤러와 서비스 계층은 이 모델 스테이트를 통해 속삭이는 중인거죠. 다시말하면, 서비스 계층은 ASP.NET MVC 프레임워크의 한 특별한 기능(model state)에 의존적이다고 볼수 있는거죠.

그래서! 우리가 원하는건 뭐?!
가능한한 많이 컨트롤러 계층으로부터 서비스 계층을 분리시키자는 겁니다. 이 서비스 계층은 ASP.NET MVC 어플리케이션 뿐만아니라 다른 어플리케이션에서도 사용되고 있습니다. (저는 지금 자바 프로젝트 중이라 스프링 프레임워크를 쓰고 있는데요. 역시 서비스 계층이 존재합니다.) 자, 서비스 계층으로 부터 모델 스테이트에 의존하고 있는 것을 제거하는 방법은 무엇일까요?

일단, 더는 서비스 계층에서 모델 스테이트를 사용하지 말아보죠. 대신 IValidationDictionay 인터페이스를 구현한 클래스를 사용하겠습니다.

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

namespace Validation1.Models
{
    public class TelDirService : ITelDirService
    {
        private IValidationDictionary _validationDictionary;
        private ITelDirRepository _repository;

        public TelDirService(IValidationDictionary validationDictionary, ITelDirRepository repository)
        {
            _validationDictionary = validationDictionary;
            _repository = repository;
        }

        protected bool ValidateTelDir(TelDir telDirToValidate)
        {
            if (string.IsNullOrEmpty(telDirToValidate.Name))
                _validationDictionary.AddError("Name", "이름이 필요해.");
            if (string.IsNullOrEmpty(telDirToValidate.Phone))
                _validationDictionary.AddError("Phone", "전화번호가 필요해.");
            if (telDirToValidate.SpeedDial.HasValue)
            {
                if (telDirToValidate.SpeedDial < 1 || telDirToValidate.SpeedDial > 99)
                    _validationDictionary.AddError("SpeedDial", "단축다이얼 다시 입력해.(1~99)");
            }
            return _validationDictionary.IsValid;
        }

        public bool CreateTelDir(TelDir telDirToCreate)
        {
            if(!ValidateTelDir(telDirToCreate))
                return false;
            try
            {
                _repository.CreateTelDir(telDirToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public IEnumerable<TelDir> ListTelDir()
        {
            return _repository.ListTelDir();
        }
    }

    public interface ITelDirService
    {
        bool CreateTelDir(TelDir telDirToCreate);
        IEnumerable<TelDir> ListTelDir();
    }
}

기존과 바뀐부분은 ModelStateDictionary 부분이 IValidationDictionary 인터페이스로 바뀐것. 그뿐인거죠.
IValidationDictionary 인터페이스는 다음과 같이 정의되어있습니다. 정말 간단하죠? ㅡ.ㅡ

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

다음은, IValidationDictionary 인터페이스를 구현한 ModelStateWrapper 클래스입니다. 이 ModelStateWrapper 클래스는 모델 스테이트 딕셔너리를 생성자에서 받음으로 객체화할수 있습니다.

using System.Web.Mvc;

namespace Validation1.Models
{
    public class ModelStateWrapper : IValidationDictionary
    {
        private ModelStateDictionary _modelState;

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

        #region IValidationDictionary 멤버

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

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

        #endregion
    }
}

마지막으로, 컨트롤러의 생성자에서 서비스 계층을 생성할때 ModelStateWrapper을 이용한 것으로 수정하겠습니다.

public HomeController()
{
      _service = new TelDirService(new ModelStateWrapper(this.ModelState), new TelDirRepository());
}

휴, IValidationDictionary 인터페이스와 ModelStateWrapper 클래스를 이용해서 컨트롤러 계층에서 서비스 계층을 분리해봤습니다. 서비스 계층은 더이상 model state에 의존적이지 않죠.

암튼. 공부할 것이 참 많아요.. 그쵸?^^;

참고 : http://www.asp.net/mvc/tutorials/validating-with-a-service-layer--cs

'.NET > MVC Basic' 카테고리의 다른 글

ASP.NET MVC - Validation(4)  (0) 2010.05.27
ASP.NET MVC - Validation(2)  (0) 2010.05.25
ASP.NET MVC - Validation(1)  (0) 2010.05.24
ASP.NET MVC - Model(2)  (4) 2010.04.04
ASP.NET MVC - Model(1)  (0) 2010.03.31