MVVM 디자인 패턴

지난 주 MVP 패턴을 공부하고 난 후 생각이 든 것은 MVVM과의 차이점이었다. PresenterViewModel, 이 둘의 용어적 차이는 그 차이가 무엇인지 뚜렷하게 말해주지 않는다.

MVVM 패턴에 대해서 알아보겠다.

MVVM가 나온 배경

MVVM 패턴은 Microsoft의 엔지니어인 Ken Cooper와 Ted Peters에 의해 만들어졌다. 이 패턴은 Windows의 그래픽 프레임워크인 WPF와 Silverlight에서 처음 적용되었다. 이 패턴의 주요 목적은 To simplify event-driven programming user interfaces인데, 이것을 위해 View에 관한 로직과 비즈니스 로직을 철저히 구분한다. 여기서 사용되는 주요한 개념이 Data binding이다.

MVVM: Model-View-ViewModel

MVVM 패턴은 MVC 패턴과 마찬가지로 Model과 View를 가지고 Controller 대신 ViewModel이라는 개념을 도입했다.

각각의 개념들을 살펴보면 다음과 같다.

  • Model: 기존 패턴들의 Model과 같이 실제적 데이터를 가지며 View와는 독립되어 있다. ViewModel이 소유하고 갱신하며 가공하여 View에 표시한다.
  • View: UIView와 UIViewController가 여기에 속하며 말 그대로 보여주는 작업과 유저 인터랙션을 받는 역할을 한다. 유저 인터랙션을 받아 ViewModel에게 명령(Command)를 내린다.
  • ViewModel: Model을 가공해 View에 전달하거나 유저 인터랙션이 올 경우 그에 따른 작업을 수행한다. 작업이 끝난 후 View를 이에 맞춰 바꿔줘야 하는데 데이터 바인딩을 통해서 이를 달성한다.

장점

MVC 패턴의 View와 Model의 독립성을 유지하고 MVP 패턴과 같이 보여주는 로직과 비즈니스 로직을 나누었다. 물론 테스트 또한 나누어 실행할 수 있어 효율적인 유닛 테스트가 가능하다.

단점

데이터 바인딩이 필수적으로 요구된다. 다양한 방법을 통해 바인딩이 가능하지만 그 작업을 위해 Boilerplate code를 짜야 한다. 그래서 간단한 View나 로직을 만들 때는 배보다 배꼽이 더 큰 경우를 볼 수도 있다.

Boilerplate code: 작지만 대체할 수 없고, 여러 곳에 포함되어야 하는 코드 섹션. 프로그래머가 매우 작은 일을 하기 위해서 많은 코드를 작성해야 하는 경우를 말한다.

MVP와의 차이

MVP 패턴에서는 Presenter를 통해서 MVVM의 ViewModel과 거의 같은 역할을 한다. 그러나 MVP 패턴은 Presenter가 View와 ‘1 대 1’ 관계를 맺어야 한다는 점에서 한계를 가진다. 특정 View의 Presenter는 그 View에 특정되어진 로직을 가지고 있기 때문에 비슷한 역할을 하는 View의 경우 그 Presenter들은 중복되는 로직을 가지는 한계를 갖는다.

반면, MVVM 패턴에서 ViewModel은 View를 전혀 모른다. View만 ViewModel을 인스턴스로 갖고 있고 데이터 바인딩을 통해 ViewModel의 데이터를 View에 표시한다. 때문에 굳이 ‘1 대 1’ 관계로 묶여 있지 않고 필요에 따라 ‘多 대 多’ 관계도 가능하다. 그렇기 때문에 중복 로직을 줄일 수 있고 결합도를 낮출 수 있다.

Data Binding

데이터 바인딩의 개념은 쉽게 말해 Model과 UI 요소 간의 싱크를 맞춰주는 것이라 할 수 있다(정확히 말하면 UI 데이터 바인딩이지만 iOS를 다루기 때문에 이것을 다룬다). 이 패턴을 통해 View와 로직이 분리되어 있어도 한 쪽이 바뀌면 다른 쪽도 업데이트가 이루어져 데이터의 일관성을 유지할 수 있다.

iOS에서 데이터 바인딩을 하는 방법은 다음과 같다.

  • KVO
  • Delegation
  • Functional Reactive Programming
  • Property Observer

우리는 이 중에서 Property Observer를 사용해 볼 것이다.

MVVM 사용 예제

두 텍스트 필드에 사람의 이름과 나이를 넣으면 그 아래에 있는 레이블이 그것을 표시하는 앱을 생각해보자.

먼저 모델을 만들 것이다. 간단하게 Person 구조체로 나타낼 수 있다.

1
2
3
4
struct Person {
var name: String
var age: Int
}

그 다음, Person을 표시할 ViewModel을 만들 것이다.

1
2
3
struct PersonViewModel {
var person: Person
}

ViewController에서는 UITextFieldDelegate의 메서드인 textFieldDidEndEditing를 통해 텍스트 필드에 변화가 일어나면 레이블에서 단순히 뿌려준다.

1
2
3
4
5
6
7
8
9
10
11
12
extension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
guard let nameText = nameTextField.text,
let ageText = ageTextField.text,
let age = Int(ageText) else { return }

personViewModel.person.name = nameText
personViewModel.person.age = age

yearLabel.text = nameText + " " + "\(age)세"
}
}

이 상태에서는 UI의 변화가 ViewModel에 영향을 끼치고 있다. 즉, 방향이 하나인 것이다. 이것을 프로퍼티 옵저버를 통해 데이터 바인딩을 하여 양방향으로 해보자.

먼저, didSet을 통해 Person의 변화를 감지한다. 변화 이벤트가 일어날 시 적절한 처리를 할 객체가 필요한데 이것에는 다양한 방법을 사용할 수 있다. 그 중 클로저를 사용해보자. bind(lisenter: Listener?) 메서드를 통해 외부에서 PersonViewModel의 Listener 타입의 listener를 지정할 수 있게끔 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct PersonViewModel {
typealias Listener = (Person) -> Void
var listener: Listener?

var person: Person {
didSet {
listener?(person)
}
}
...
mutating func bind(listener: Listener?) {
self.listener = listener
}
}

그러면 ViewController에서는 다음과 같이 바인딩을 하면 된다.

1
2
3
4
5
6
override func viewDidLoad() {
...
personViewModel.bind { [weak self] person in
self?.yearLabel.text = person.name + " " + "\(person.age)세"
}
}

이를 통해 textFieldDidEndEditing에서 Person이 바뀌었을 시에, 즉 ViewModel이 바뀌었을 때 자동으로 yearLabel이 업데이트될 것이다.

텍스트 필드도 delegate를 사용하지 않고 데이터 바인딩을 통해 로직을 수행할 수 있다. UITextField를 상속하는 BindingTextField 클래스를 만들어 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
class BindingTextField: UITextField {
var textChanged: (String) -> Void = { _ in }

func bind(callBack: @escaping (String) -> Void) {
textChanged = callBack
addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}

@objc func textFieldDidChange() {
guard let text = text else { return }
textChanged(text)
}
}

BindingTextField는 위에서와 마찬가지로 클로저 textChanged를 통해 이벤트를 감지할 시 일어날 작업을 정의한다. 또 bind 메서드를 통해 textChanged에 인자로 받은 callBack 클로저를 넣어주고 더불어 editingChnaged 때 일어날 메서드를 등록해둔다. 그래서 텍스트 필드 안의 값이 변경될 시 textChnaged 클로저가 실행된다.

ViewController에서는 다음과 바꿔주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@IBOutlet weak var nameTextField: BindingTextField! {
didSet {
nameTextField.bind { [weak self] name in
self?.personViewModel.person.name = name
}
}
}
@IBOutlet weak var ageTextField: BindingTextField! {
didSet {
ageTextField.bind { [weak self] ageText in
guard let age = Int(ageText) else { return }
self?.personViewModel.person.age = age
}
}
}

느낀 점

iOS를 시작한 지 얼마 안 되었을 때 MVC가 아닌 다른 디자인 패턴을 배울 때는 ‘굳이 배울 필요가 있나?’라는 생각이 많이 들었다. 하지만 공부를 계속 하고 앱을 직접 만들다 보니 Controller가 커지는 것을 실감할 수 있었고 너무 많은 것들이 여기에 들어간다고 직접 느꼈다. 그래서 다시 디자인 패턴을 공부해보자하는 생각에서 지난주와 이번주 주제를 정하게 되었다. 많은 자료들을 뒤져가며 공부를 했지만 아직도 제대로 개념이 잡히지는 않는다. 하나씩 적용해보면서 익혀가는 것이 답일 듯 싶다.

Reference