momodudu.zip

#11 MVC와 MVVM 패턴에 관하여 본문

ios/Swift

#11 MVC와 MVVM 패턴에 관하여

ally10 2020. 8. 28. 11:10

 

사실 iOS에 국한되는 포스팅은 아니긴 하지만, 카테고리가 없고 swift예제를 쓸거기 때문에 일단 Swift 카테고리에...

 

디자인패턴은 사실 내가 제일 약한부분이다 (ㅠㅠ) 전회사에서나 학부생때나 이런거에 대해서 깊게 고민해본적이 없기도 하고.. 사실 어떻게 따져보면 방법론적인 이야기뿐이라 사실 나에게는 잘 와닿지 않는 이야기라서..? 어찌됐든 그래도 공부해야할건 공부해야되니까!

이 포스팅을 쓰면서도 MVC와 MVVM에 대해 개념적(?)으로는 와닿지만, 사실 실무에서 어떻게 쓰는지 코드를 직접 짠게 아니라서 확 와닿는건 아니다.. 틀린것도 있을 수 있고.... 이건 일단 공부해놓으면 나중에 접목시킬때가 오게 되겠지 뭐...

 

궁극적으로 말하고자 하는건 MVVM패턴이지만, MVVM패턴을 알려면 MVC도 알아야된다!

 

# MVC ( Model-View-Controller )

MVC에는 총 3가지의 element가 존재한다. Model은 데이터 그 자체를 의미하고, View는 데이터를 화면에 뿌려주는 element다. 모바일로 치면 유저가 보는 화면 그 자체를 의미한다고 보면 되겠다. Controller는 이 두 element간의 중재를 담당한다고 보면 되는데, Model을 View에 뿌려주기 위해 여러가지 유저로부터의 interaction을 담당한다.

 

사실 디자인패턴 얘기가 제일 싫은이유가.. 이런 개념적인 이야기만 늘어놓기때문에 정말정말*100 와닿지 않는다.. 예를 들어보면, Profile이라는 Model이 있다고 치자. 이 Profile은 이름, 성별, 생년월일 등 ... 여러가지 정보를 포함한다. 그리고 이 Profile을 간단하게 화면에 띄우고자 한다. 여기서 "화면" 이 바로 View다. 

 

그렇다면 Controller는? 사실 Controller는 Swift에선 ViewController에 해당하는데, Swift의 ViewController의 의미는 "View와 Controller"가 아니라, "View를 Control 한다"의 의미라고 한다. 즉 위 예시에서는 구체적으로 "뷰에 버튼이 있다면, 버튼을 클릭한 사용자의 action을 받아와서 프로필을 출력한다"가 Controller의 role이라고 볼 수 있다. 사용자와의 interaction을 view로부터 받아와서 이에 대한 동작들을 정의해서 뷰를 갱신하는등의 말그대로 컨트롤 역할을 담당한다.

 

아.. 역시 말로 레디오 구구 레디오 절절 써놓으니까 어지럽다. 예제를 보자.

 

 

아주 간단한 예제로, 위처럼 버튼을 클릭하면 2개의 UILabel의 "me"/현재 날짜 를 profile로 갖는 Model을 표시해보고자 한다. 일단 간단하게 MVVM을 적용하지 않은 보통의 코드는 아래와 같을것이다.

 

// Model
class Profile {
    var name: String
    var birthDay: Date
    
    init(name: String, birthDay: Date) {
        self.name = name
        self.birthDay = birthDay
    }
}

 

Profile 데이터 자체를 나타내는 Model 클래스다. 보다시피, Profile은 string type의 name property와 Date type의 birthday 프로퍼티를 가지고 있다. 이제 이 모델을 표시해주는 뷰 컨트롤러의 코드를 보자.

 

class ViewController: UIViewController {
    
    required init?(coder: NSCoder) {
        _profile = Profile(name: "me", birthDay: Date())
        
        super.init(coder: coder)
    }
    
    public override func viewDidLoad() {
        _nameField.text = ""
        _birthdayField.text = ""
    }
    
    func getBirthdayString(_ birthDay: Date) -> String {
        let calendar = Calendar(identifier: .gregorian)
        let components = calendar.dateComponents([.year, .month, .day], from: birthDay)
        
        
        return "\(String(components.year!)) / \(String(components.month!)) / \(String(components.day!))"
    }
    
    @IBAction func onClicked(_ sender: Any) {
        _nameField.text = _profile.name
        _birthdayField.text = getBirthdayString(_profile.birthDay)
    }
    
    private let _profile: Profile
    @IBOutlet var _nameField: UILabel!
    @IBOutlet var _birthdayField: UILabel!
    
}

 

버튼을 클릭하면 뷰 컨트롤러가 생성될 때 만들어놓은 profile이 보이도록 작성했다. 일단 profile의 name은 view의 컴포넌트중 하나인 UILabel에 잘 표시될것이고, 그렇다면 Date는 어떻게 표시해야할까? Date를 나타내는 view의 컴포넌트 타입은 없다. 즉, 스트링으로 변환을 해서 UILabel에 표시해야 한다는 점이다. 이를 위해 getBirthdayString()이라는 함수를 만들었다.

 

이 아주아주 간단한 코드에서만 봐도... ViewController의 역할이 뭔지 잘 와닿지 않는다. MVC에서 C가 하는일이 도대체 뭔데? 이론상으로는 이렇다. Model은 데이터를 가지고 있고(name, birthday) View는 이것을 뿌려주는 역할만 한다. 즉, Model이 어떤 컴포넌트로 구성되어있고 뭘 가지고 있고 무슨타입인지 몰라야한다. 이 두 사이의 데이터 컨버팅 역할을 해주는게 Controller의 역할일까? 그렇다면 View로 부터 인풋을 받아와서 출력하고... 하는등의 처리도 controller의 역할일까?

아주 간단한 코드에서 더 발전해서, 만약 데이터 json데이터같은걸 파싱해야하는 경우가 생기면 이건 어디에 때려박아야 하는지?

 

위에서 나열한 역할들을 모두 ViewController에 때려박다보면... Controller의 코드는 매우 장황해지고, 솔직히 맡은바가 뭔지 specific하지 않다. 이런 구체적이지 않은 컴포넌트는 소프트웨어공학적인 입장에서는 매우 거슬린다. 사람들이 MVC패턴을 Massive View Controller라고 비꼬는 이유도 여기에 있다. ViewController의 역할이 모호하고, 이일 저일 다 도맡아서 하게 된다. 사실 이게 꼭!!! 나쁘다. 정말 안좋은 패턴이다. 라고 단언할 수 없다. 장점이 있으니까 이름도 붙은 패턴이니께.... 다만 쓰다보니, 뷰 컨트롤러에 다 때려박다보면 디버깅도 어렵고, 단위테스트가 어렵다는 단점이 있다. 그리고 개발자 입장으로서 봤을 때 클린하지 못한것은 사실이다. 나만해도 테스트코드는 그냥 뷰 컨트롤러에 다때려박는데, 코드 라인수가 어마어마하다.

 

이를 보완하고자 나온 패턴이 바로 ~ MVVM 패턴이다.

 

# MVVM(Model-View-Veiw Model)

MVVM은 Model, View, View Model 총 3개가 존재한다. 여기서 View와 Model은 기존의 역할을 그대로 진행한다. View Model의 경우에는, Model의 Data를 View에 representable 가능하게끔 래핑? 을 하는 역할이라고 보면 되겠다.

위에서 언급했던 Controller의 역할 중에 뷰를 컨트롤 하는 역할을 제외하고, Model의 데이터를 View가 표시할 수 있도록 변환하는 역할을 한다.

 

요것도 코드를 보자..

 

일단 Model은 그대로일것이고, Massive Controller로부터 뷰에 표시하기 위해 데이터 컨버팅하는부분을 뜯어내야한다! 즉, 위 MVC코드에서 getBirthDayString()같은 과정을 View Model로 떼어내고자 한다. 

// Model
class Profile {
    var name: String
    var birthDay: Date
    
    init(name: String, birthDay: Date) {
        self.name = name
        self.birthDay = birthDay
    }
}

// View Model
class ProfileViewModel {
    private let profile: Profile
    
    public init(profile: Profile) {
        self.profile = profile
    }
    
    func getBirthDayString() -> String {
        let calendar = Calendar(identifier: .gregorian)
        let components = calendar.dateComponents([.year, .month, .day], from: self.profile.birthDay)
        
        return "\(String(components.year!)) / \(String(components.month!)) / \(String(components.day!))"
    }
    
    public var nameString: String {
        get {
            self.profile.name
        }
    }
    
    public var birthDayString: String {
        get {
            self.getBirthDayString()
        }
    }
}

Profile 모델은 그대로고, View Model이 새롭게 추가되었다. initializer로 프로필 데이터 모델을 받아오고, 내부적으로 birthday string을 String으로 변환해서 name/birthday를 반환해준다. 이렇게되면 일단 ViewModel의 객체는 UI와 전혀 상관없는 독립적인 단위 클래스가 되는것에 주목하자.

 

class ViewController: UIViewController {
    required init?(coder: NSCoder) {
        _profile = Profile(name: "me", birthDay: Date())
        
        super.init(coder: coder)
    }
    
    public override func viewDidLoad() {
        
    }
    
    @IBAction func onClicked(_ sender: Any) {
        _viewModel = ProfileViewModel(profile: _profile)
    }

    
    fileprivate func fillUI() {
        guard let viewModel = _viewModel else {
            return
        }
        
        self._nameField.text = viewModel.nameString
        self._birthdayField.text = viewModel.birthDayString
    }
    
    var _viewModel: ProfileViewModel? {
        didSet {
            self.fillUI()
        }
    }
    
    var _profile: Profile
    @IBOutlet var _nameField: UILabel!
    @IBOutlet var _birthdayField: UILabel!
    
}

 

그리고 뷰 컨트롤러의 코드다. 생성자에서 프로필을 생성하는부분은 똑같다.

 

    @IBAction func onClicked(_ sender: Any) {
        _viewModel = ProfileViewModel(profile: _profile)
    }

기존 예제와 똑같이 버튼을 클릭하면 프로필을 보여주는 동작을 할 건데, 여기서는 viewModel을 생성한다.

 

    var _viewModel: ProfileViewModel? {
        didSet {
            self.fillUI()
        }
    }

이부분을 보면 viewModel이라는 property가 하나 더 생겼는데, didSet property observer를 달아서 세팅되었을때 fillUI함수를 호출한다. 버튼을 클릭했을때 뷰컨이 생길때 만들어놓은 프로필을 인자로 viewModel이 생성되고, property observer에 의해 fillUI()가 호출되는 시퀀스다.

 

    fileprivate func fillUI() {
        guard let viewModel = _viewModel else {
            return
        }
        
        self._nameField.text = viewModel.nameString
        self._birthdayField.text = viewModel.birthDayString
    }

 

fillUI()함수인데, name/birthday 필드에 텍스트를 세팅하는 부분 보면 model의 데이터를 가져와서 date를 변환하고 구구절절..이 아니라 viewModel의 getter로 간단하게 세팅하고 있는것을 볼 수 있다. 즉! Controller는 model의 데이터가 어떤 타입으로 이루어져 있는지 신경쓰지 않는다. 단지 각 필드에 맞는 부분을 채울뿐이다. Controller에서는 birthday가 원래 Date였던것을 몰라도 된다는 점이다.

 

이렇게 되면 Controller는 데이터 변환이나 파싱 등등.. 이런 역할을 덜어낼 수 있고, 오로지 viewModel을 UI에 갖다붙이는 역할과, 버튼 클릭등의 IBAction등을 처리만 하면 된다. 그래서 컨트롤러의 코드 무게가 훨씬 가벼워진다.

뿐만아니라, 기존의 controller코드는 데이터를 다루는 부분마저도 UIKit과 너무 밀접하게 붙어있었기 때문에 단위테스트가 어렵다. 그렇지만 지금은 데이터부분이 Model-View Model로 빠지면서 View Model은 UI에 독립적인 클래스가 되었다. 그렇기때문에 단위테스트가 용이해진다는 장점도 생긴다!

 

 

간단한 느낌으로 개념만 살펴봤는데.. 개념은 이해가 가긴 하는데, 아직 실전에 접목하기에는 좀 연계가 잘 안된다고 해야되나.. 무튼 좀 어렵다. MVVM은 좋긴 한데 설계가 어렵다는 단점이 있다고 한다. 암튼~ 그때그때 맞는거 잘 쓰면 되겄쥬~

 

 

'ios > Swift' 카테고리의 다른 글

#10 Swift - Protocol as Interface  (0) 2020.04.29
#9 Swift - class, struct에 대해서 다시 짚어보기  (0) 2020.04.23
#8 Swift - Automatic Reference Counting  (0) 2019.12.28
#7 Swift - Protocol(1)  (0) 2019.11.28
#6 Swift - Extension  (0) 2019.11.28