MVP 디자인 패턴

애플의 기본적인 디자인 패턴은 Model-View-Controller로 이루어진 MVC 패턴이다. 엄밀히 말하면 애플이 제공하는 MVC는 기존에 나온 MVC와는 View와 Model을 독립적으로 나눈다는 점에서 기존의 MVC를 개선시켰다고 할 수 있다. 그러나 Controller가 비즈니스 로직과 View의 관한 코드 모두를 포함해 너무 비대해진다는 단점은 많은 개발자들이 새 디자인 패턴을 찾도록 이끌었다.

오늘은 그 중 하나인 MVP 패턴에 대해서 볼 것이다.

MVP: Model-View-Presenter

MVP 패턴은 기존의 Model과 View는 가져가되, Presenter라는 개념을 도입했다.

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

  • Model: 기존의 Model 역할과 같다. 실제적 데이터를 가지고 있고 이것을 Presenter가 소유하고 갱신하는 역할을 한다. View와는 독립되어 있다.
  • View: iOS의 UIView와 UIViewController가 여기에 속하며 모든 비즈니스 로직은 Presenter에 맡긴다. 그래서 Passive View 라고도 불린다.
  • Presenter: UIKit을 사용하지 않는 비즈니스 로직을 모두 수행한다. 또 Model의 데이터를 가공해 View에서 보여주기 위한 UI 친화적인 포맷으로 바꾸는 역할도 한다.

장점

기존의 MVC 패턴이 가지고 있는 장점을 유지한 채 보여주는 부분과 비즈니스 로직 부분을 나눠 모듈화의 정도를 심화시켰다. 이는 테스트가 각각의 부분에서 용이하게 이뤄질 수 있다는 것을 의미한다.

단점

Presenter라는 새로운 역할이 추가되었기 때문에 코드 양의 증가를 들 수 있다.

MVP 사용 예제

다음은 MVP 패턴에 맞춰 하나의 뷰를 완성해보는 예제이다.

먼저 User 모델을 만든다. 성과 이름, 이메일, 그리고 나이에 대한 정보를 갖고 있다.

1
2
3
4
5
6
struct User {
let firstName: String
let lastName: String
let email: String
let age: Int
}

그 다음 UserService 클래스를 만든다. 이 클래스는 비동기적으로 유저의 데이터를 받는 역할을 할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class UserService {
func getUsers(completionHandler: @escaping ([User]) -> Void) {
// Users Mock 데이터
let users = [User(firstName: "Junesang", lastName: "Yu", email: "alpaca@test.com", age: 25),
User(firstName: "Kangsoo", lastName: "Lee", email: "dough@test.com", age: 26),
User(firstName: "Jiyong", lastName: "Jeong", email: "yongbyung@test.com", age: 27)]

// 비동기로 받아오는 것처럼 가정
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
completionHandler(users)
}
}
}

다음은 Presenter 차례이다. 먼저 User를 View에서 보여주는 방식으로 가공하는 UserViewData를 만든다.

1
2
3
4
struct UserViewData {
let name: String
let age: String
}

그 다음, View의 추상화 작업을 진행한다. 프로토콜을 통해 Presenter가 ViewController를 알지 못해도 View의 작업을 수행할 수 있도록 한다.

1
2
3
4
5
6
protocol UserView {
func startLoading()
func finishLoading()
func setUsers(users: [UserViewData])
func setEmptyUsers()
}

위의 두 가지를 활용해 Presenter를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class UserPresenter {
private let userService: UserService
private weak var userView: UserView?

init(userService: UserService) {
self.userService = userService
}

func attachView(view: UserView) {
userView = view
}

func detachView() {
userView = nil
}

func getUsers() {
userView?.startLoading()
userService.getUsers { [weak self] users in
DispatchQueue.main.async {
self?.userView?.finishLoading()
}

guard !users.isEmpty else {
DispatchQueue.main.async {
self?.userView?.setEmptyUsers()
}

return
}

let userViewDatas = users.map {
UserViewData(name: "\($0.firstName) \($0.lastName)", age:"\($0.age) years") }
DispatchQueue.main.async {
self?.userView?.setUsers(users: userViewDatas)
}
}
}
}

Presenter가 가지고 있는 userService와 userView는 각각 Model과 View를 의미한다. attachView(view:) 메서드를 통해 View와 연결해준다. 그리고 getUsers() 메서드를 통해 userView에 뿌려줄 모델을 가공하고 넣어 주는 역할을 수행한다. UI를 수정하는 작업은 메인 큐에서 수행하도록 한다.

마지막 남은 View 부분을 작성한다. UIViewController에 UserView 프로토콜을 익스텐션으로 추가하여 프로토콜의 함수들을 실제로 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class UserViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

private let userPresenter = UserPresenter(userService: UserService())
private var usersToDisplay = [UserViewData]()

private let cellIdentifier = "UserCell"

override func viewDidLoad() {
super.viewDidLoad()

tableView.dataSource = self
activityIndicator.hidesWhenStopped = true

userPresenter.attachView(view: self)
userPresenter.getUsers()
}
}

extension UserViewController: UserView {
func startLoading() {
activityIndicator.startAnimating()
}

func finishLoading() {
activityIndicator.stopAnimating()
}

func setUsers(users: [UserViewData]) {
usersToDisplay = users

tableView.isHidden = false
tableView.reloadData()
}

func setEmptyUsers() {
tableView.isHidden = true
}
}

extension UserViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return usersToDisplay.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)

let userViewData = usersToDisplay[indexPath.row]
cell.textLabel?.text = userViewData.name
cell.detailTextLabel?.text = userViewData.age

return cell
}
}

유닛 테스트

좋은 디자인 패턴의 요소 중 빠질 수 없는 것이 테스팅의 용이성이다. MVP 패턴을 사용하면 UIViewController를 테스트하지 않고 UI 로직을 테스트하는 것이 가능하다.

Reference