MVVM 패턴의 고찰

오늘은 MVVM 패턴에 대한 이해를 빠르게 가져보고 MVVM 패턴을 고찰해 보는 시간을 가져 보겠습니다.

MVVM 패턴이란?

MVVM 패턴은 모델-뷰-뷰 모델(Model-View-Viewmodel, MVVM) 아키텍처 패턴으로 마이크로소프트의 켄 쿠퍼(Ken Cooper)와 테드 피터스(Ted Peters)에 의해 만들어졌습니다. 이는 모델-뷰-바인더(model-view-binder)의 일종 입니다.
다른 아키텍처 패턴으로 MVC(Model-View-Controller), MVP(Mode-View-Presenter)가 있고, 최근에 MVU(Model-View-Update) 패턴을 살펴볼 수 있습니다.

MVVM 패턴에서 뷰모델은 특이한 포지션을 가지는데, 모델에 노출하는 역할을 합니다. 모델을 어떻게 뷰에 "전달"하는가에 대한 모든 책임을 지게 됩니다.

MVVM출처:위키백과

MVVM 패턴의 구성 요소

  • 모델(Model) : 도메인 모델 또는 데이터 모델
    도메인 모델은 행위과 상태를 모두 가지는 추상화 모델입니다. 일반적으로 객체지향의 객체라 이해할 수 있습니다.
    데이터 모델은 데이터의 상태에 집중하는 방식으로 데이터 접근 레이어(Data Access Layer, DAL)를 통해 속성으로만 구성되는 객체입니다. 대표적인 예로 ORM의 모델이 됩니다.

  • 뷰(View) : 사용자 화면에 보여지는 구조, 배치, 외관
    뷰는 곧 사용자 유저 인터페이스 입니다. 하지만 MVVM 패턴의 뷰는 어떻게 "동작"하느냐가 아니라 어떻게 "보여지느냐"만 담당합니다. 뷰의 동작은 뷰모델에서 처리됩니다.

  • 뷰모델(View Model) : 뷰의 추상화 모델(화면이 없는 뷰)
    뷰모델은 "화면이 없는 뷰"입니다. 뷰모델이 뷰가 "동작"하도록 만듭니다. 뷰모델이 뷰를 동작하도록 만들기 위한 메커니즘으로 바인딩을 사용합니다. 그렇기 때문에 MVVM을 MVB(Model-View-Binder)라고도 합니다.

MVVM 고찰

MVVM 구성요소

뷰(View)

MVVM에서 뷰는 오직 어떻게 "보여지느냐"에만 집중해야 합니다. 어떻게 "동작하느냐"는 것은 모두 뷰모델에서 처리되어야 합니다. 이는 뷰는 단위테스트를 자동화 할 수 없기 때문에 중요합니다.
뷰의 동작을 뷰모델에 위임하는 방법은 바인딩입니다. 바인딩이란 뷰모델의 값이 변경되거나, 바인딩된 뷰의 컨트롤에서 해당 값을 변경했을 때 상호 작용이 일어나는 것을 말합니다. 바인딩을 통해 뷰모델의 값을 변경하는 것으로 뷰의 동작을 제어할 수 있게 됩니다.

이때 중요한 것은 뷰모델이 뷰에 대한 의존성이 없어야 하는데 있을 경우 단위테스트를 진행할 수 없게 되기 때문입니다. 또한 의존성이 발생하면 잘 구현된 뷰모델을 다른 뷰에 적용할 수 없게 됩니다.

뷰모델(View Model)

일반적으로 뷰모델을 MVC의 컨트롤러 정도로 생각하는 경향이 있는데 아닙니다. 뷰모델은 추상화된 뷰로 보이지 않는 뷰의 완전한 동작을 구현해야 합니다. 하지만 뷰모델은 보여지지 않는 상태로 구성되므로 이를 뷰의 모델(View Model)이라 합니다.
만약 뷰모델이 뷰가 어떻게 "동작하는지"를 달성했다면 뷰모델의 단위테스트를 통해 MVVM 패턴을 적용한 어플리케이션의 뷰의 동작을 단위테스트 할 수 있게 됩니다.
실제로 마이크로소프트에서 제공하는 컨트롤은 뷰모델에 해당하는 객체를 통해 단위테스트를 수행하고 있습니다.

모델(Model)

모델은 도메인 모델 및 데이터 모델이 대상이 될 수 있으며 정보를 제공하는 모든 대상이 모델이 될 수 있습니다. 일반적으로 화면과 관련 없는 비즈니스 로직이 모델이 됩니다. 화면 동작에 관련 있는 비즈니스 로직은 뷰모델에 배치될 수 있습니다. 하지만 뷰모델 또한 모델이므로 뷰모델이 뷰모델을 모델로 사용할 수 도 있습니다.

뷰-뷰모델-모델의 의존관계

뷰-뷰모델의 의존 관계

  • 뷰는 뷰모델의 속성을 바인딩하여 이용하므로 뷰모델과 의존 관계를 가집니다.
  • 뷰모델은 단지 바인딩할 수 있도록 속성을 노출할 뿐이므로 뷰와 의존 관계를 가지지 않습니다.

뷰-모델의 의존 관계

  • 뷰는 뷰모델에서 제공하는 모델을 사용하므로 모델과 의존 관계를 가집니다.
  • 모델은 어떤 뷰를 사용하는지 알지 못하므로(알 필요가 없으므로) 뷰와 의존 관계를 가지지 않습니다.

뷰모델-모델의 의존 관계

  • 뷰모델은 모델을 이용하므로 모델과 의존 관계를 가집니다.
  • 모델은 어떤 뷰모델이 모델을 사용하는지 알 필요가 없으므로 뷰모델과 의존 관계를 가지지 않습니다.

image.png

뷰모델이 뷰의 요소를 참조할 필요가 있을 경우

이런 경우를 생각해봅시다.
가령 F2 단축키를 눌렀을 때 선택된 요소에 텍스트 박스가 활성화 되어 텍스트를 입력할 수 있어야 하며 입력이 완료되면 입력된 값이 선택된 요소에 반영되어야 한다고 가정해 봅시다. 이는 뷰모델의 바인딩된 속성에 따라 뷰에서 적절히 텍스트 박스를 보이게 하고 관련처리를 진행할 수 있지만 뷰모델이 뷰의 텍스트 박스에 접근할 수만 있다면 좀 더 간단한 로직으로 로직이 구성될 수 도 있습니다. 이처럼 다양한 이유로 뷰모델이 뷰의 요소에 접근해야 하는 이유가 있습니다.
이때 의존성을 해치지 않으면서 뷰모델이 뷰의 요소에 접근하는 방법은 객체지향 설계 SOLID원칙 중 의존관계 역전 원칙을 적용 하는 것입니다. 이는 MVVM 패턴처럼 각 구성요소가 명확히 구분된 의존 관계를 역전할 수 있게 합니다.

image.png

뷰에서 제공하는 기능을 IView 인터페이스로 노출하고 뷰모델은 뷰가 아닌 IView를 참조하는 방식입니다. 뷰의 구성요소 또한 이렇게 인터페이스로 뷰 모델에 노출할 수 있고 인터페이스로 추상화 및 캡슐화 되므로 뷰모델이 뷰를 직접 참조하지 않아도 뷰의 요소에 접근할 수 있게 됩니다.

정리

간략히 MVVM 패턴에 대해 정리해 보았습니다. MVVM 뿐만 아니라 MVU등 최신 트랜드에 맞는 다양한 아키텍쳐 패턴이 있으니 두루 살펴보면 좋을 것 같습니다.

17개의 좋아요

오… IView를 이용한 방식은 생각을 못했었습니다. 전 단순하게 그럴 필요가 있는 경우에는 그냥 Behavior를 사용하거나 Service를 이용했었습니다…ㅎㅎ 뭔가 프레임워크에 의존하지 않고 써먹을 수 있는 방법같아서 나중에 한 번 써보고 싶습니다.

저도 MVVM 개념이 감이 안와서 질문하는 분들께 단순하게 '유닛테스트가 가능하게 객체를 구성’하라고 일러줍니다. ㅎㅎ

현 회사에서 WPF를 거의 처음 접했고 사수 없이 하다보니 MVVM에 대한 것은 본래 객체지향의 확장버전이라 개념을 학습하는게 어렵지 않았는데 사실 XAML이 더욱 어려운 것 같습니다. 사수 있는 곳에서 XAML도 잘해보고 싶네요…ㅠ

4개의 좋아요

뷰모델에서 ListBox에서 선택한 데이터를 가져오려고 뷰의 요소를 사용할일이 있었는데, IView로 한번 해봐야겠네요. 좋은글 감사합니다 ^^

3개의 좋아요

그러면 View 클래스들은 직접적으로 Window 클래스를 상속받지 않고, 새롭게 만든 IView가 Window클래스를 상속받고, View가 IView를 상속하게 해서 뷰모델에 사용하는 것이 맞나요?

2개의 좋아요

아닙니다. 여기서 IView는 인터페이스이고 View 클래스가 IView의 인터페이스를 구현하게 합니다. 기존 View에서 달라지는 것은 IView의 인터페이스 구현 정도이고 나머지 상속관계라던가 구조는 동일하게 가져갑니다.

ViewModel에서 참조하는 정보는 IView만 참조하게 됩니다. 이렇게 해서 ViewModel이 View를 직접 참조하지 않으면서 View의 기능을 IView를 통해 사용할 수 있게 됩니다.

2개의 좋아요

그럼 본문 예제대로면, TextBox가 상속하는 ITextBox를 만들어서 ViewModel이 View에 있는 TextBox를 참조하여 기능을 구현하는게 아니라, View와 의존 관계가 없는 ITextBox를 참조한다는 말씀이신건가요?

2개의 좋아요

네 맞습니다. View에서 ViewModel로 노출하고자 하는 기능을 IView(이름은 말씀하신 것 처럼 ITextBox가 될 수도 있고요)를 ViewModel에 노출해 주는 식입니다. 이 때 IView가 인터페이스여야 하는 이유는 View의 기능에 직접 참조하지 않아야 하기 때문이고요, 그 제한 안에서라면 어떤 구성이든 괜찮습니다.

좀 더 부연 설명하자면, 노출하는 인터페이스 역시 화면 관련 객체를 보유하고 있는 것은 아니므로 (실제로 인스턴스는 공유될 수 있으나! 노출되는 기능은 인터페이스 기능입니다.) 뷰 모델의 한 구성이라고 할 수 있습니다.

혼란스러우실 수 도 있을 것 같아 좀 더 부연을 하겠습니다.

View의 인스턴스를 view라고 하고,
View가 IView 인터페이스를 구현했을 때 IView의 기능중 SetVisible()을 가정해봅시다.
그리고 View는 Window라는 뷰의 구성 클래스를 상속 구현했다고 전제를 하고요,

View의 인스턴스인 viewView 클래스 타입으로 전달하는 것은 ViewModel이 View를 참조해서는 안되는 제한에 위배되므로 그렇게 해서는 안되나,

View가 구현한 IView로 ViewModel에서 view인스턴스를 참조(이때 참조 유형은 IView입니다)하게 하는 것은 IView가 View의 구성이 아니므로 괜찮아 집니다. 이것을 의존관계 역전이라고 합니다.

5개의 좋아요

답글 감사합니다. MVVM에서의 의존 역전은 덕분에 이해가 됐습니다.
꼬리를 무는 질문이 되는데요… 저는 지금껏 VIew ↔ ViewModel의 기능 구현을 위해 커맨드 패턴을 사용을 했습니다. 이렇게 하니까, 작은 기능을 구현하려고 해도 생성해야 하는 속성과 필드, 그리고 xaml의 속성까지 많은 부분의 추가가 필요해서 '너무 오버하는거 아닌가?'라는 의문이 들었습니다. 그런데 위 포스트를 보니, 확실히 로직이 간단해질 수도 있겠다라는 생각이 들었는데요.

여기서 궁금해지는 것이 view 객체들을 인터페이스화해서 뷰에서 뷰모델에 넘겨주려고 하면, code behind에서의 처리 작업은 피할 수 없다고 생각이 드는데요. 속성으로 인스턴스를 넘겨주든, 뷰모델의 생성자로 넘겨주는 로직을 code behind에 작성하는 것이 MVVM 패턴에 위배되지 않은 행위가 되나요?

3개의 좋아요

네. 위배되지 않습니다. MVVM의 경계가 잘 지켜지는 것 (위의 도식을 참고), View와 ViewModel, Model이 명확히 구분되어 구분된 목적에 맞게 구조 및 구현되는것이 중요하고, code behind유무는 사실 MVVM 패턴과 상관이 없습니다.

4개의 좋아요

저도 “왜 굳이 가운데에 뷰 모델이 따로 있어야 하느냐?”의 답은 단위 테스트를 하기 위해서라고 생각합니다. 그런데 저는 이걸 살짝 비틀어서 받아들이기도 했어요. 거꾸로 현실적으로 뷰는 (러프한 종단 테스트 내지는 기능 테스트를 수행하는 걸 제외하면) 자동화된 단위 테스트가 불가능하다는 것을 인정하는 데에서 시작한 발상 같다고 생각했거든요.

뷰는 어차피 이상적인 형태의 단위 테스트가 불가능하다. 그럼 테스트를 하기 어려운 것들(버튼의 크기가 어떤지, 색이 어떤지, 이런 것들은 어려우니까)은 나중에 생각하기로 하고, 그럼에도 그 중에서 테스트를 해볼 만한 부분들이 있을 것이다. 그럼 그것들을 테스트하기 좋게라도 분리하면 어떨까?

이런 현실론에 입각해서 “그래도 단위 테스트할 수 있는 부분”의 영역이 뷰 모델이 아닐까… 그렇다면 어디까지 뷰 모델에 넣고 어디서부터 뷰에 넣어야 할지는 단순한 공식이 있다기 보다는 그 프로젝트의 맥락이나 팀이 처한 상황 등에 따라 얼마든지 유연하게 정할 수 있는 것 같고, 또 그런 유연함이 뷰 모델의 장점인 것 같다는 생각도 듭니다.

5개의 좋아요

동의합니다.

5개의 좋아요