ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Architecture Patterns ( MVC, MVP,MVVM ) ( with iOS, Swift)
    Architecture + Design Pattern 2021. 10. 21. 11:33

    안녕하세요, 드디어 Architecture Patterns 에 대해 알아볼 시간입니다.

    해당 글은 https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52 에 대한 번역과 함께 본인의 생각을 담아 작성하겠습니다. 

     

     

    먼저, 왜 architecture 에 대해 고민해야할까요 ? 

    가장 큰 이유는 아마도 app 이 커지면 커질수록 유지보수 하기가 어려워질 수 있기 때문일겁니다. Apple 에서는 MVC Pattern 을 Guide 로 주었지만 사실 MVC 에는 많은 문제가 있어요. (저도 경험하고 싶지 않았습니다.. 진작 architecture 에 대한 공부를 더 할걸 그랬어요..)

     

    앞으로 3가지 pattern (출처에는 VIPER 까지 포함하지만)  MVC, MVP, MVVM 에 대해 알아보도록 하겠습니다! 

     

    Pattern 을 하나하나 자세히 보기 전에, 어떤 요소들이 architecture 에서 중요하게 고려되는지 먼저 볼게요.

     

    1. Distribution of responsibilities

    각 entity (Model, View 등 ) 의 역할은 고를수록, 분명할 수록 좋습니다. 분배가 잘 될수록 우리의 뇌도 코드가 어떻게 작동하는지 이해하기 쉽습니다. 코드를 최대한 쉽게 하기 위해서는 SRP(single-responsibility principle) 을 따라야 합니다. (코드를 쉽게하기 위해서 이기도 하지만 모든 코드를 짤 때 항상 염두해두어야 합니다. )

     

    single-responsibility principle
    각 module, class, function 은 프로그램서 '단 하나' 의 기능에 대해서만 맡도록 해야한다는 이론. 

     

    2. Testability 

    테스트가 쉽게 가능할수록  좋습니다. ( 중요합니다.. !! ) Test 를 통해서 developers 은 runtime 시 효율적으로 버그를 찾아낼 수 있습니다. 

    3. Ease of use, low maintenance cost

    사용하기 쉬울수록, 그리고 유지보수 하기 쉬울수록 좋습니다. 최고의 코드는 아무것도 쓰여져 있지 않은 코드라는 말이 있습니다. 코드가 적을수록 버그도 적습니다. 

     

     

     

    MV(X) essentials

    아래와 같은 다양한 Architecture Design Patterns 가 있습니다. 

    이 중 처음 세개 (MVC, MVP, MVVM ) 는 app 의 entities 를 3 종류로 분류합니다. 

     

    • Models — domain data , 또는 data access layer ( 데이터 다루는 역할 ) 입니다. 'Person' 또는 'PersonDataProvider' classes 가 되겠습니다.
    • Views — GUI (Graphical User Interface , 보이는 화면 ) 입니다. iOS 에서는 앞에 'UI' 가 들어간 것들이 되겠습니다.
    • Controller/Presenter/ViewModel — Model 과 View 를 접착, 또는 중개해주는 역할입니다. 유저의 action 에 따라 Model 을 바꿔주거나, Model 이 바뀔 때 View 를 바꿔줍니다. 

     

    이렇게 역할을 나눔으로써 우린 각 역할 및 코드를 더 잘 이해할 수 있고, 재사용 (특히 View, Model) 하기 편해지며, 개별로 테스트 할 수 있게 됩니다. 

     

     

     

    1. MVC ( Apple's MVC) 

    Expectation

    첫번째로, 아마 다들 익숙하리라고 생각되는 MVC Pattern 입니다. Controller 는 View 와 Model 사이 중개역할이고, 따라서 View , Model 은 서로에 대한 정보를 갖지 않습니다. 가장 재사용이 힘든 것은 Controller 이지만, Model 에서 처리하기 부적합한 데이터 처리를 모두 Controller 에 맡기면 되기 때문에 이 점은 봐줄만합니다. 이론적으로는 직관적이지만 좀.. 뭔가 이상한 것 같죠? 가끔 MVC 는 'Massive View Controller' 라 불립니다. 또한 view controller offloading ( view controller 짐 줄여주기.. ) 이 iOS 개발자들 사이에서 중요한 topic 이 되었습니다. 왜 이렇게까지 되었을까요 ?? 

     

    Reality

     

    Cocoa MVC 의 Controllers 가 View 의 life cycle 과 너무 관련이 깊기 때문에 서로 분리되어있다고 하기가 어렵고, 따라서 Massive View Controllers 가 되기 쉽습니다. ViewController 에서 logic, data transformation 을 Model 에 맡기면 VC 의 할 일이 줄어들긴 하겠지만 View 가 대신 해줄 수 있는 일은 많이 없고, 대부분 View 는 Controller 에 user actions 를 전해주는 역할을 합니다. 결국 view controller 는 모든 것의 delegate, 그리고 datasource 가 되고, 또한  dispatching, canceling network request, 등등등 의 역할도 맡습니다. 

     

     

    아래와 같은 코드를 많이많이 보셨을겁니다. 

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

    cell (View) 이 직접적으로 Model 과 configure 되었으므로 이는 MVC guideline 을 위배하지만, 항상 일어나고 뭔가 잘못되었다는 느낌을 보통 받지 않습니다. 만약 MVC 를 철저하게 따른다면, cell 은 controller 내에서 configure 하고 View 에 Model 을 건네주지 않을 것이기 때문에, 이렇게 하면 Controller 는 더욱 비대하게 됩니다. 

    Cocoa MVC is reasonably unabbreviated as the Massive View Controller.
    (ㅠㅠ....번역: MVC 가 Massive View Controller 라 불리는 데엔 타당한 이유가 있다.. )

    문제는 Unit Testing 을 하기 전까지는 분명하지 않을 수 있습니다. view controller 가 view 와 강하게 결합되어있기 때문에 테스트를 하려면 최대한 business logic 과는 연결되지 않게, 그리고 views 의 life cycle 을 고려하여야 합니다. (쉽게말해 테스트 하기 매우 어렵습니다. )

     

    예제 코드를 봅시다.

    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;

     

    MVC 를 assembling 은 view controller 에서 해도 됩니다. 

     

    음.. 매우 테스트하기 어려워보이죠 ? GreetingModel class 는 분리해서 테스트할 수 있지만, 어떠한 presentation logic 도 UIView 와 관련된 methods 호출 (viewDidLoad, didTapButton) 없이는 GreetingViewController 에서 할 수 없습니다.  (해당 methods 는 모든 views 를 loading 시킬 것이기 때문에 unit test 에 매우 좋지 않습니다. ) 실제로 UIViews 를 simulator 에서 테스트 할 때 다른 기기에서도 똑같이 잘 작동할 것이라고 보장할 수 없기때문에, "Host Application" 을 Unit Test target configuration 에서 제거하고, simulator 에서 application 없이 테스트 해보기를 권장합니다. 

    View 와 Controller 사이 interaction 은 Unit Tests 로 테스트할 수 없습니다.

    지금까지를 고려해봤을때, Cocoa MVC 는 안좋은 pattern 으로 보입니다만, 글 초기에 언급했던, Architecture 에 있어 중요한 세가지 요소들을 기준으로 다시 한번 평가해보겠습니다. 

     

     

    • Distribution  ViewModel 은 분리되어있지만, ViewController 가 강하게 묶여있습니다. 
    • Testability — Distribution 이 좋지 않기 때문에, Model 만 Test 할 수 있을 거에요..
    • Ease of use — 다른 patterns 과 비교했을 때 가장 적은 양의 코드만을 갖습니다. 또한, 모든 iOS 개발자들이 이 Pattern 에 익숙할 것이기 때문에 경험이 없는 개발자라고 쉽게 유지보수 가능합니다.

    Cocoa MVC 는 architecture 에 많은 시간을 투자하기 힘들 때, 또는 조그마한 project 에 많은 유지비용이 과분하다 싶을 때 적합합니다. 

     

    Cocoa MVC 는 개발 속도면에서 architectural patterns 중 가장 빠릅니다.

     

     

     

     

    2. MVP

    Cocoa MVC’s promises delivered

     

    Apple의 MVC 와 똑같이 생기지 않았나요 ? 맞아요, 그렇지만 이 architcture 의 이름은 MVP 입니다. 그렇다고해서 Apple 의 MVC 가  MVP 인건 아니에요. Apple 의 View 는 Controller 와 tight 하게 묶여있지만, MVP 의 mediator 인 Presenter 는 view controller 의 life cycle 와 아무런 연관이 없기 때문에 View 가 더 쉽게 Test 될 수 있어요. Presenter 는 layout code 와 아무런 관련이 없지만, data 와 state 를 이용하여 View 를 update 시켜주어야 합니다.  

     

     

    What if I told you, the UIViewController is the View.

    (여기서는 UIViewController 가 View 입니다 .) 

    MVP 에서, UIViewController 의 subclasses 는 Presenter 가 아닌, View 입니다. 이에 따라 test 가 가능해지지만, 직접 데이터와 event binding 을 만들어주어야 하기때문에 development speed 는 그만큼 느려집니다. 

     

    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

    코드에서 볼 수 있듯이, MVC 에서보다 훨씬 길죠 ?? 

     

    Important note regarding assembly

    MVP pattern 에는 세가지 분리된 구조로부터 생기는 assembly 에 대한 문제가 있습니다. View 가 Model 에 대해 아는 것은 원하지 않기 때문에, assembly 를 view 에서 하는 것은 적합하지 않습니다. 따라서, 어딘가 다른 곳에서 해야하죠. 예를들면 app-wide Router service 를 만들어 assemly 를 그곳에서 시켜주고, View-to-View presentation 을 할 수 있습니다. 이러한 문제는 MVP 뿐만 아닌 앞으로 나올 patterns 에 대해서도 발생하게 됩니다. 

     

    이제 MVP 의 특징에 대해 알아보아요.  

     

    • Distribution — Presenter, Model 에서 대부분의 과정을 맡습니다. (View 는 딱히..  위 예제에서는 Model도 뭘 안하지만)
    • Testability — 매우 좋습니다. View 가 하는 일이 매우 적어졌기 때문에 대부분의 logic 을 test 할 수 있습니다.
    • Easy of use — 예제 코드에서는 MVC에 비해 코드의 양이 두배정도 되었습니다. 

    iOS 에서 MVP 는 테스트에 대한 많은 용이성과 동시에 많은 코드 양을 가집니다. 

    (출처에서는 약간 다르게 생긴 MVP 에 대해서도 잠깐 다루지만,  잘못 만들어진 형태이기 때문에 이 글에서는 다루지 않겠습니다. )

     

    3. MVVM

    The latest and the greatest of the MV(X) kind

    The MVVM is the newest of MV(X) kind thus, let’s hope it emerged taking into account problems MV(X) was facing previously.

    In theory the Model-View-ViewModel looks very good. The View and the Model are already familiar to us, but also the Mediator, represented as the View Model.

     

    MVVM 은 가장 따끈따끈한 MV(X) pattern 이에요. 그저 전에 (MVC, MVP) 가지고 있던 문제들을 잘 해결했기를 바랍니다. 

    이론적으로 볼때 Model-View-ViewModel 은  아주아주 좋아보여요. View, Model, Mediator (View Model) 모두 어느정도 익숙해졌죠 ?? 

     

    MVVM

    MVVM 은 MVP 와 여러 측면에서 꽤 유사합니다. 

     

     

    It is pretty similar to the MVP:

    • MVVM 은 view controller 를 View 로 취급합니다.
    • View 와 Model 사이 큰 연결고리가 없습니다. 

    추가로, 여기서는 'Binding' 을 사용합니다. ( View ~ View Model ) 그 외에도 ViewModel 은 View 와 state 의 UIKit independent representation 입니다. View Model 은 Model 에 변화가 일어날 시 알려줄 것을 요청하고, Model 이 update 될 때 View Model ( self) 또한 update 합니다. 이때, View 와 View Model 사이에 Binding 이 있기 때문에, View 또한 update 되게 됩니다. 

     

    Bindings

    MVP 에서 아주 짧게 Binding 을 언급했었어요. 여기서 더 알아보도록 해요.  Binding 은 OS X Developement 에서 시작되었지만, iOS toolbox 에는 존재하지 않아요. 물론 Binding 외에도 KVO (Key-value observing) , notification 이 있긴 하지만 bindings 만큼 편하지 않습니다. KVO notification 을 쓰지 않는다고 했을 때, 두가지 options 가 있습니다. 

     

    요즘 MVVM 에 대해서 들을 때면 아마 ReactiveCocoa 가 떠오를거에요. MVVM 은 간단한 binding 으로도 만들 수 있지만, Reactive Cocoa 를 이용하면 MVVM 의 이점을 최대한으로 이용할 수 있습니다. 

    Reactive frameworks 에는 그리 달갑지 않은 진실이 있어요 : 'the great power comes with the great responsibility'. Reactive 를 사용하면 mess up 하기도 쉽게 돼요. 즉, 어떤 것을 잘못하게 되면 디버깅 하는 데에 엄청난 시간이 필요해집니다. 

    아래 call stack 을 한번 봅시다. 

    In our simple example, the FRF framework or even the KVO is an overkill, instead we’ll explicitly ask the View Model to update using showGreeting method and use the simple property for greetingDidChange callback function.

    우리의 작고 귀여운 example 에서 FRF framework , KVO 는 overkill 이므로, 대신 View Model 에게 explicit 하게 update 를 요청하겠습니다. (showGreeting, greetingDidChage callback function 이용해서 ) 

    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

     

    이제 또 다시 돌아온 feature assessment 시간입니다. 

    • Distribution — 샘플코드가 작아서 명확하게 확인하기 힘드시겠지만, MVVM 의 View 는 MVP 의 View 보다 많은 responsibilities 를 갖습니다. MVVM 에서 View는 bindings 을 만들어놓고 ViewModel 에서 state 를 바꾸는 반면 MVP 에서는 Presenter 에게 모든 것을 맡기고 스스로 update 하지는 않기 때문입니다. (the first one updates it’s state from the View Model by setting up bindings)
    • Testability — View Model 은 View 에 대해 아무것도 모르기 때문에 테스트 하기 쉽습니다. View 또한 test 될 수 있지만, UIKit depedant 이기 때문에 skip 하고 싶으실 겁니다.
    • Easy of use — 샘플 에서는 MVP 에서와 비슷한 코드양을 가지나, 실제 앱에서는 MVVM 에서 bindings 을 사용하신다면 훨씬 적은 코드로 만들 수 있습니다.
    MVVM 은 이미 전에 언급했던 approaches 의 이점들을 결합한, 그렇다고 bindings 때문에 코드가 많이 길어지지도 않는 아주아주 매력적인 architecture 에요. 이러한 이점을 갖는 도중에도 testability 또한 괜찮은 수준입니다.

     

    Conclusion

    지금까지 여러 architecutal patterns 를 살펴보았습니다. 겪고 계신 문제에 대한 해답을 어느정도 찾으실 수 있으셨다면 좋겠네요.. 그러나 어떠한 것도 '어느것에나 써도 항상 최고' 인 그런 architecture 은 없다는 것을 느끼셨을거에요. 따라서 architecture pattern 을 고르는 것은 tradeoffs 에 달렸다고 보면 좋겠네요. 

    따라서, 하나의 앱 내에서도 mix 된 architectures 을 갖는게 오히려 자연스럽습니다. 예들 들면 MVC 로 시작했지만 어떠한 한 화면이 MVC 를 통해 유지보수 하기 너무나 힘들 때에는 MVVM 등의 pattern 으로 해당 화면('만')을 바꾸시는 거죠. 두 architures 가 쉽게 compatible 가능하기 때문에 MVC Pattern 을 통해 잘 작동하는 다른 화면들 또한 refactor 할 필요는 없습니다. 

    Make everything as simple as possible, but not simpler — Albert Einstein

     



     

    personal review : 지금까지 접하고 만들었던 어떠한 자료들보다 저에게는 가장 많은 도움이 된 것 같네요. (정말로..) (( Refactoring 중이었던 계산기 앱 내 기본 계산기는 (from MVC ->) MVP 로 바꾸고, TabBarController, Settings pages 는 MVC 를 그대로 가져가도 될 것 같아요. Refactoring 을 마치면 다음 앱을 RxSwift 와 함께 MVVM Pattern 으로 제작하면 될 것 같습니다 (메모 or 날씨 앱))  이미 많이 review 된 페이지를 번역하며 공부한 거라 이미 해당 원본 게시글을 접하신 분들도 계시겠지만 이 글이 조금의 도움이라도 되면 좋겠습니다. 긴 글 읽어주셔서 감사합니다. 

     

     

     

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

    'Architecture + Design Pattern' 카테고리의 다른 글

    ReactorKit  (0) 2022.10.12
    Clean Architecture + MVVM  (0) 2022.10.03
    MVVM  (0) 2022.09.26
    Clean Architecture  (0) 2022.09.25
    Delegate & Protocol  (0) 2022.03.28
Designed by Tistory.