본문 바로가기

iOS

나의 iOS 아키텍처 패턴 일대기: MVVM

이번 글에서는 그동안 경험했던 아키텍처 패턴들을 시간 순서대로 나열하면서 왜 사용하게 되었는지, 각 패턴들의 장단점이 무엇이였는지 정리를 해보려고 해요. 패턴들을 적극적으로 사용하기만 하고 패턴별로 어떤점이 좋았는지, 불편했는지에 대해서 코딩을 직접 하면서 느끼기만 했지, 명시적으로 정리를 해본적이 없어서 머릿속에 흐릿하게만 기억되고 있었어요. 이번 글을 통해서 선명하게 기억을 남겨보려고 합니다!

이전 패턴 글은 아래 링크로 이동해주세요!
나의 iOS 아키텍처 패턴 일대기: MVC


아래에 기록될 내용들은 제가 직접 사용해보면서 느꼈던 장단점입니다.
즉, 제 개인적인 견해일뿐 정답은 아닙니다! 더 좋은 장점이 있다던지, 불편한 점을 해결한 방법이 있다면 같이 공유하면 더 좋을 것 같아요!

아키텍처 패턴 사용 순서

사용해본 아키텍처 패턴들이 많지는 않습니다! 위에 그려진 것처럼 3개만 사용했는데요. 각 패턴들을 사용했던 이유와 패턴들마다 느꼈던 장단점을 간단한 예시와 함께 정리하려고 합니다.

MVVM

MVVM (출처: 위키피디아)

MVVM은 GUI 코드로 구현하는-그래픽 사용자 인터페이스(뷰)의 개발을 비즈니스 로직 또는 백-엔드 로직(모델)로부터 분리시켜서 뷰가 어느 특정한 모델 플랫폼에 종속되지 않도록 해준다. 라고 위키에 정의되어있습니다. MVC에서 불편했던 단점들이 어느정도 보안된 아키텍처 패턴이라고 느껴졌습니다!.
MVC편에서 기록해둔 단점이 모든 로직들이 뷰컨트롤러에 의존성이 걸려있다는 것이였는데요! MVVM에서는 그런 의존성들을 분리시켜줍니다. 뷰모델이 비즈니스로직만 담당하기 때문에 뷰로부터의 의존성이 사라지게 됩니다.

일단 말로 했을 때는 개념적으로는 어느정도 이해했는데 막상 구현하려고 하니 어떻게 해야할지 감이 안가는데요..
일단 MVC에서 구현했던 앱을 MVVM으로 만들어봅시다! (MVVM에서는 Rx를 사용하도록 하겠습니다!)

예시 화면에는 UILabel과 UIButton이 하나씩 있고 버튼을 누를 때마다 UILabel에 터치 횟수를 출력해줍니다.
3번을 초과한 버튼 터치가 있을 경우 터치 한도를 초과했다는 Alert을 띄워주는 간단한 앱입니다!
이 화면을 MVVM로 표현한다면 뷰모델을 아래처럼 표현할 수 있을 것 같아요.
(이전 MVC 코드가 궁금하신 분들은 이전 글로 가면 볼 수 있습니다!)

import RxSwift
import RxRelay

final class ViewModel {
  let input = Input()
  let output = Output()
  
  private let disposeBag = DisposeBag()
  private let maxTouchCount = 3
  private var currentTouchCount = 0
  
  struct Input {
    let tapButton = PublishSubject<Void>()
  }
  
  struct Output {
    let touchCount = PublishRelay<Int>()
    let showErrorAlert = PublishRelay<Void>()
  }
  
  
  init() {
    self.input.tapButton
      .subscribe(onNext: self.onTapButton)
      .disposed(by: self.disposeBag)
  }
  
  private func onTapButton() {
    if self.isUnderMaxTouch(count: self.currentTouchCount) {
      self.increaseTouchCount()
    } else {
      self.output.showErrorAlert.accept(())
    }
  }
  
  private func isUnderMaxTouch(count: Int) -> Bool {
    return count < self.maxTouchCount
  }
  
  private func increaseTouchCount() {
    self.currentTouchCount += 1
    self.output.touchCount.accept(self.currentTouchCount)
  }
}

이전 MVC에서 작성했던 로직들 중에 이벤트를 전달 받고 처리하는 프레젠테이션 로직들을 뷰모델로 분리시켰습니다.
단순하게 생각하면 뷰컨트롤러에서 사용자 입력 부분과 출력 부분을 추상화 시킨다고 생각하면 됩니다!
뷰모델 안에 Input, Output 두가지 타입을 구현했는데요, Input은 사용자로부터 입력을 받는 부분이에요. 위 예제에서는 버튼 입력받는 이벤트입니다. OutputInput으로 들어온 입력들을 처리하고 뷰에서 다시 보여줘야할 녀석들입니다. 위 예제에서는 뷰에서 처리해줘야할 Output이 두개가 있어요! 터치 횟수를 보여주는 것과 최대 터치 회수를 넘겼을 때, Alert을 띄워주는것 두개가 있습니다.

이렇게 Input Output을 정의해준뒤 뷰모델에서 로직들을 처리하게되면 MVC에서 단점으로 생각했던 뷰컨이 뷰에 의존되어있던 것을 해결할 수 있습니다. 또한 이렇게 의존성이 사라지게 되면 뷰모델만 독립적으로 테스트도 가능해집니다. Input을 통해 테스트를 원하는 값을 넣을 수 있고 그 값에 따라 원하는 Ouput이 나오는지 확인할 수 있습니다.
이렇게 처리하고 뷰컨트롤러에서 뷰모델 바인딩은 다음과 같이 진행합니다!

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  @IBOutlet weak var button: UIButton!
  
  private let viewModel = ViewModel()
  private let disposeBag = DisposeBag()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    self.bindViewModel()
  }
  
  private func bindViewModel() {
    self.button.rx.tap
      .bind(to: self.viewModel.input.tapButton)
      .disposed(by: self.disposeBag)
    
    self.viewModel.output
      .touchCount
      .bind(onNext: self.setButtonCount(count:))
      .disposed(by: self.disposeBag)
    
    self.viewModel.output
      .showErrorAlert
      .bind(onNext: self.showErrorAlert)
      .disposed(by: self.disposeBag)
  }
  
  private func setButtonCount(count: Int) {
    self.titleLabel.text = "\(count)"
  }
  
  private func showErrorAlert() {
    // Alert 출력
  }
}

뷰컨에서는 뷰모델에서 정의한 Input Output에 연결만 해주면 됩니다! 뷰에서는 로직과 관련된 것들 없이 뷰에서만 하는 함수들만 구현해주면 됩니다.

이렇게 뷰모델을 구현해서 MVVM으로 구현해서 사용해봤는데요!
이 패턴으로 사용하면서 느낀점을 다시한번 정리해보고 다음에는 현재 사용하고 있는 ReactorKit에 대해서 정리해볼께요!


간단요약

MVVM

장점

  • 뷰모델은 뷰컨트롤러에서 로직들만 독립시켜 놓게되어서 MVC에서 뷰컨트롤러의 막대한 역할을 줄여줄 수 있습니다.
  • 로직들만 분립시킬 수 있으니 뷰모델만 따로 테스트가 가능해졌습니다.

단점

  • MVVM이라는 아키텍처 자체를 이해하고 구현을 실제로 해보는데까지의 시간이 오래걸렸습니다.
  • MVVM 패턴이 rx랑도 잘 어울리다보니 rx도 같이 익혀야해서 러닝커브가 높다고 생각되었습니다.
  • MVC패턴에 비해서 간단한 화면을 만드는데에는 많은 코드가 생성됩니다 (화면 하나 만드는데, 뷰, 뷰컨, 뷰모델 3가지 파일이 생성됩니다.)