본문 바로가기

iOS 개발/App 개발 관련

[iOS] Architecture patterns(MVC,MVP,MVVM)

<본론에 앞서>

 

1.설계를 생각해야 하는 이유.

-큰 규모의 소프트웨어를 제작할때, 설계 구조를 생각하지 않으면 관리가 어렵다.(버그를 찾기 힘들다..)

→ 즉, 소프트웨어를 이해하기 좋고, 분리되어 있어 테스트가 용이하며, 각각을 재사용할 가능성이 생긴다.

 

2.좋은 설계란?

-기능의 책임이 균형있게 분산된 설계.

-테스트가 용이한 설계.

-사용이 쉽고, 유지 비용이 적은 설계.

 

3.Model? View? Controller(Presenter,ViewModel)?

-Model: data에 접근,수정하는 계층. Domain data에 대한 책임이 있다.

-View: 표현 계층에 대한 책임.(ios 개발 시 'UI'로 시작하는 것들!)

-Controller(Presenter,ViewModel): View와 Model 사이를 중개. View에서의 사용자 동작에 반응하여 Model을 변경하고, 변경된 Model을 기반으로 View를 갱신한다.

 

<MVC>

 -애플은 초기에 잘 분리된 View와 Model을 Controller가 중개하는 설계 구조를 계획했지만, 현실에서 Controller는 View의 life cycle과 연관성이 커서 사실상 분리된다고 보기 힘들어졌다. (Massive View Controller라는 우스갯소리까지..)

 

 -많은 경우에 View는 View에서 발생한 모든 action을 Controller에 보내어 Controller를 비대하게 만들고, (delegate, data source, 네트워크 요청 처리 etc..) 여기에서 우리가 View로 책임을 옮길 수 있는 방법은 많지 않다.

 

 

1
2
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier"as UserCell
userCell.configureWithUser(user)
cs

 위 코드에서 userCell (userCell은 View이다.) 은 아랫줄에서 user (user는 Model이다.) 와 직접적으로 상호작용하여 설정된다. 이처럼 MVC의 가이드 라인을 위반하는 일이 항상 일어난다. 

 가이드 라인대로라면 코드 아랫줄처럼 Model을 View에게 전달하지 않고 Controller에서 설정되어야 하는데, 이런 방식은 Controller를 더욱 비대하게 한다. (즉, MVC를 가이드 라인대로 잘 작성하면 Massive View Controller가 된다..)

 

 

 이런 문제는 Unit testing에서 두드러진다.

-위에서 언급하듯 View와 Controller는 긴밀하게 연결되기 때문에, 여러분의 business logic이 view layout code와 분리된 방식대로 Controller의 test code를 작성하려면, View와 해당 life cycle에 대해 아주 창의적으로 모의 code를 작성해야 한다. 따라서, test가 쉽지 않다. 

 

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
import UIKit    
 
struct Person { // Model
    let firstName: String
    let lastName: String
}
 
class GreetingViewController : UIViewController { // View + Controller
    var person: Person!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }
    
    func didTapButton(button: UIButton) {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.greetingLabel.text = greeting
        
    }
    // layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
cs

 위와 같은 MVC 구성에서는 GreetingViewController 내부에서 viewDidLoad, didTapButton와 같은 UIView에 연관된 메서드를 직접 호출하지 않고는 presentation logic을 테스트할 수 없다. (이런 호출은 모든 View를 로딩하게 될 가능성이 있고, 이는 unit testing에서 원하는 바가 아니다.)

 

※ '좋은 설계'로서의 MVC

1.분산의 관점: View와 Model은 분리되지만, View와 Controller는 강하게 연결(균형있게 분산되지 않음).

 

2.테스트의 관점: 분산이 좋지 않으므로 편향된 테스트를 할 가능성.

 

3.사용성의 관점: 설계 패턴 중 코드 양이 가장 적고, 가장 친숙.(사용성은 좋다.)

 

※결론

다른 설계 패턴을 도입할 여유가 없고 프로젝트 규모가 작다면 MVC도 괜찮은 선택!

 

<MVP>

-View와 Model의 중개자로 Presenter가 도입. Presenter는 MVC의 Controller와 달리 View와의 연관성이 아주 적으므로 test시 View의 모의 code를 쉽게 작성할 수 있다. 따라서, Presenter에는 layout code가 없고 View를 최신 데이터와 상태로 갱신할 책임만을 가진다. 

-MVP에서는 UIViewController의 subclass들은 Presenter가 아닌 View이다.

 

 

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
s
import UIKit
 
struct Person { // Model
    let firstName: String
    let lastName: String
}
 
protocol GreetingView: class {
    func setGreeting(greeting: String)
}
 
protocol GreetingViewPresenter {
    init(view: GreetingView, person: Person)
    func showGreeting()
}
 
class GreetingPresenter : GreetingViewPresenter {
    unowned let view: GreetingView
    let person: Person
    required init(view: GreetingView, person: Person) {
        self.view = view
        self.person = person
    }
    func showGreeting() {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.view.setGreeting(greeting)
    }
}
 
class GreetingViewController : UIViewController, GreetingView {
    var presenter: GreetingViewPresenter!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }
    
    func didTapButton(button: UIButton) {
        self.presenter.showGreeting()
    }
    
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    
    // layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
 
cs

-MVP는 세개의 분리된 계층을 가짐으로써 생기는 assembly(세개의 계층을 조합)의 문제점을 처음 드러낸 설계 패턴이다. 우리는 View가 Model에 대해 모르길 원하지만 위의 코드에서는 함께 조합하고 있다. (이 문제는 이후의 모든 패턴에서 확인할 수 있다.)

 

※ '좋은 설계'로서의 MVP

1.분산의 관점: Presenter와 Model의 책임을 거의 분리했다. (View는 좀 부실해졌지만..)

 

2.테스트의 관점: 아주 좋다. 대부부의 business logic을 테스트해볼 수 있다.

 

3.사용성의 관점: 의도는 명료하지만 코드의 양만 봤을때 MVC의 두배 가량.

 

※결론

ios에서 MVP는 "테스트의 용이성 + 많은 코드 양" 을 의미한다. 

 

<MVVM>

-MVVM에서 view controller는 View로 취급(MVVM에도 역할이 적은 view controller가 존재).

-View는 Model과 연관성이 적고 View model 사이에는 binding을 가진다.

-ios에서 View model은 당신의 View와 그 상태에 대한 UIKit-독립적 표현이다. 

-View model은 Model의 변화를 불러와 스스로 갱신, View와 View model은 binding되어 있으므로 View 역시 따라서 갱신. 

 

-View에서 Model을 다양한 형태로 표현하고 싶을때(e.g. Date형식을 String, Decimal등으로 표현하고 싶을때), View model이 변환을 담당해줄 것이다. (MVC는 이 작업을 view controller에 맡기겠지만 거기선 이미 viewDidLoad, IBActions등의 일이 많으므로 Massive View Controller가 되는 것!)

 

 

 

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
import UIKit
 
struct Person { // Model
    let firstName: String
    let lastName: String
}
 
protocol GreetingViewModelProtocol: class {
    var greeting: String? { get }
    var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
    init(person: Person)
    func showGreeting()
}
 
class GreetingViewModel : GreetingViewModelProtocol {
    let person: Person
    var greeting: String? {
        didSet {
            self.greetingDidChange?(self)
        }
    }
    var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
    required init(person: Person) {
        self.person = person
    }
    func showGreeting() {
        self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
    }
}
 
class GreetingViewController : UIViewController {
    var viewModel: GreetingViewModelProtocol! {
        didSet {
            self.viewModel.greetingDidChange = { [unowned self] viewModel in
                self.greetingLabel.text = viewModel.greeting
            }
        }
    }
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
    }
    // layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
cs

 

※ '좋은 설계'로서의 MVVM

1.분산의 관점: MVVM의 View는 MVP의 View보다 책임이 더 많다. (MVVM에서 View는 View model과의 binding을 통해 스스로 갱신되지만, MVP에서는 View가 모든 event를 Presenter에게 넘기고 자신은 스스로 갱신하지 않기 때문.)

 

2.테스트의 관점: View model은 View에 대해 알지 못하므로 test를 용이하게 한다. (View는 UIKit에 의존적이므로 건너뛰어도 될 법 하다.)

 

3.사용성의 관점: View와 View model의 binding 덕분에 코드의 양이 줄어든다.(MVP에서 event를 Presenter로 넘긴뒤 수동적으로 View를 갱신하는 긴 과정과 비교해보자.)

 

※결론

MVVM은 좋다. 

 

<마무리하며>

-만능의 설계 패턴 따위는 존재하지 않는다. 여러 패턴을 조합하는 것이 자연스럽다. 가령, MVC 패턴으로 앱 제작을 시작하였으나, 어떤 화면 하나가 MVC로는 도저히 효율적이지 않을 것 같다면 해당 화면만 MVVM 패턴으로 작성할 수 있다. (대부분의 패턴은 함께 쓰는데에 문제가 없다.)  

 

 

아래 글의 일부를 번역, 요약하였습니다.

출처 https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.wtcp3gqzw

 

iOS Architecture Patterns

Demystifying MVC, MVP, MVVM and VIPER

medium.com