다음을 통해 공유


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

작성자 : Stephen Walther

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

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

우려 사항 분리

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

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

목록 1 - Models\ProductRepository.vb

Public Class ProductRepository
Implements IProductRepository

    Private _entities As New ProductDBEntities()


Public Function ListProducts() As IEnumerable(Of Product) Implements IProductRepository.ListProducts
    Return _entities.ProductSet.ToList()
End Function


Public Function CreateProduct(ByVal productToCreate As Product) As Boolean Implements IProductRepository.CreateProduct
    Try
        _entities.AddToProductSet(productToCreate)
        _entities.SaveChanges()
        Return True
    Catch
        Return False
    End Try
End Function

End Class

Public Interface IProductRepository
Function CreateProduct(ByVal productToCreate As Product) As Boolean
Function ListProducts() As IEnumerable(Of Product)
End Interface

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

목록 2 - Controllers\ProductController.vb

Public Class ProductController
Inherits Controller

    Private _repository As IProductRepository

Public Sub New()
    Me.New(New ProductRepository())
End Sub


Public Sub New(ByVal repository As IProductRepository)
    _repository = repository
End Sub


Public Function Index() As ActionResult
    Return View(_repository.ListProducts())
End Function


'
' GET: /Product/Create

Public Function Create() As ActionResult
    Return View()
End Function

'
' POST: /Product/Create

<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude:="Id")> ByVal productToCreate As Product) As ActionResult
    _repository.CreateProduct(productToCreate)
    Return RedirectToAction("Index")
End Function

End Class

서비스 계층 만들기

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

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

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

목록 3 - Models\ProductService.vb

Public Class ProductService
Implements IProductService

Private _modelState As ModelStateDictionary
Private _repository As IProductRepository

Public Sub New(ByVal modelState As ModelStateDictionary, ByVal repository As IProductRepository)
    _modelState = modelState
    _repository = repository
End Sub

Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
    If productToValidate.Name.Trim().Length = 0 Then
        _modelState.AddModelError("Name", "Name is required.")
    End If
    If productToValidate.Description.Trim().Length = 0 Then
        _modelState.AddModelError("Description", "Description is required.")
    End If
    If productToValidate.UnitsInStock

리포지토리 계층 대신 서비스 계층을 사용하도록 제품 컨트롤러가 목록 4에서 업데이트되었습니다. 컨트롤러 계층은 서비스 계층과 연결됩니다. 서비스 계층은 리포지토리 계층과 대화합니다. 각 계층에는 별도의 책임이 있습니다.

목록 4 - Controllers\ProductController.vb

Public Class ProductController
Inherits Controller

Private _service As IProductService

Public Sub New()
    _service = New ProductService(Me.ModelState, New ProductRepository())
End Sub

Public Sub New(ByVal service As IProductService)
    _service = service
End Sub


Public Function Index() As ActionResult
    Return View(_service.ListProducts())
End Function


'
' GET: /Product/Create

Public Function Create() As ActionResult
    Return View()
End Function

'
' POST: /Product/Create

<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
    If Not _service.CreateProduct(productToCreate) Then
        Return View()
    End If
    Return RedirectToAction("Index")
End Function

End Class

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

서비스 계층 분리

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

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

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

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

Public Class ProductService
Implements IProductService

Private _validatonDictionary As IValidationDictionary
Private _repository As IProductRepository

Public Sub New(ByVal validationDictionary As IValidationDictionary, ByVal repository As IProductRepository)
    _validatonDictionary = validationDictionary
    _repository = repository
End Sub

Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
    If productToValidate.Name.Trim().Length = 0 Then
        _validatonDictionary.AddError("Name", "Name is required.")
    End If
    If productToValidate.Description.Trim().Length = 0 Then
        _validatonDictionary.AddError("Description", "Description is required.")
    End If
    If productToValidate.UnitsInStock

IValidationDictionary 인터페이스는 목록 6에 정의되어 있습니다. 이 간단한 인터페이스에는 단일 메서드와 단일 속성이 있습니다.

목록 6 - Models\IValidationDictionary.cs

Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface

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

목록 7 - Models\ModelStateWrapper.vb

Public Class ModelStateWrapper
Implements IValidationDictionary

Private _modelState As ModelStateDictionary

Public Sub New(ByVal modelState As ModelStateDictionary)
    _modelState = modelState
End Sub

#Region "IValidationDictionary Members"

Public Sub AddError(ByVal key As String, ByVal errorMessage As String) Implements IValidationDictionary.AddError
    _modelState.AddModelError(key, errorMessage)
End Sub

Public ReadOnly Property IsValid() As Boolean Implements IValidationDictionary.IsValid
    Get
        Return _modelState.IsValid
    End Get
End Property

#End Region

End Class

마지막으로 목록 8의 업데이트된 컨트롤러는 생성자에서 서비스 계층을 만들 때 ModelStateWrapper를 사용합니다.

목록 8 - Controllers\ProductController.vb

Public Class ProductController
Inherits Controller

Private _service As IProductService

Public Sub New()
    _service = New ProductService(New ModelStateWrapper(Me.ModelState), New ProductRepository())
End Sub

Public Sub New(ByVal service As IProductService)
    _service = service
End Sub


Public Function Index() As ActionResult
    Return View(_service.ListProducts())
End Function


'
' GET: /Product/Create

Public Function Create() As ActionResult
    Return View()
End Function

'
' POST: /Product/Create

<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
    If Not _service.CreateProduct(productToCreate) Then
        Return View()
    End If
    Return RedirectToAction("Index")
End Function

End Class

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

요약

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