[Design Pattern] RxSwift + MVVM Pattern - iOS Architecture

2021. 2. 3. 22:42Design Pattern

반응형

어제는 VIPER Pattern에 대해 글을 작성해보았는데,

오늘은 MVVM에 대해 작성을 해보려고 합니다. (+RxSwift로 구현하는 법을 알아보려고 해요.)

 

그리고 MVVM 패턴과 VIPER 패턴을 비교하면서 MVVM 패턴에 대해 알아보겠습니다..☺️

 

이전 글을 읽고오면 비교하기 더욱 좋을 것 같습니다.

 

[Design Pattern] VIPER Pattern - iOS Architecture

iOS의 Design Pattern에는 많은 것들이 있죠...? MVC, MVVM, VIPER, RIBs, MVP등이 있는 것으로 알고있습니다. 제가 여기서 사용해본 것은 MVC, MVVM 정도입니다.. 아직 참 갈길이 먼 것 같네요 😢 오늘은 이 중에

dongminyoon.tistory.com

 

MVVM Pattern이란?

MVVM Architecture는 기본적으로 Model - View - View Model의 관계를 가지고 있는 디자인 패턴입니다. 기존의 Controller가 View와 Model의 사이에서 과도한 역할을 하면서 많은 로직들이 섞이게 되는 MVC 패턴을 대체할 수 있는(?) 패턴이라고 합니다. 하지만 기존의 MVC 패턴과 유사한 형태를 가지고 있고 다만 MVC 패턴보다 ViewController의 역할을 덜어준 로직이 분리된 패턴입니다.

 

MVVM 패턴의 모식도는 다음과 같습니다.

 

  • View(ViewController) : VIPER 패턴에서 적용했던 것과 동일하게 View의 역할을 하고 있다. 사용자의 입력을 받아들이고 화면에 데이터를 표시하는 역할이다. 주로 View Model과 상호작용하며 UIKit에 의존적인 모듈입니다.
  • View Model : VIPER Pattern의 Presenter, InteractorView Model의 역할과 비슷한 역할을 하는 것 같다. View와는 완전 분리된 모듈로 UIKit과는 관계가 없다. 프로그램이 동작할 때, 필요한 비즈니스 로직들을 가지고 있는 모듈이다.
  • Model : VIPER Pattern의 Entity와 같은 역할을 하는 모듈이다. 즉, 어떤 화면에 데이터를 보여주어야 할 때 그 정보를 가지고 있다.

 

 

Flow Of MVVM

  1. ViewModel의 데이터를 View로 바인딩시켜준다.
  2. View가 사용자의 입력을 감지하고 있다가 사용자의 입력이 들어온다.
  3. View는 ViewModel에게 알리고 특정 데이터를 필요하다고 요청한다.
  4. ViewModel은 API콜 또는 데이터를 처리하는 로직을 처리하고 Model 모듈을 처리한다.
  5. View는 ViewModel의 변화를 관찰하다가 변한 Model을 View에 표시한다.

이러한 로직으로 MVVM의 Flow가 흘러간다고 정리할 수 있다. 단편적인 예이기 때문에 더욱 많은 상황이 발생할 수 있을 것 같지만..(?) 대표적으로 데이터를 요청하고 처리하는 로직은 이정도라고 생각합니다.

 

MVVM Pattern을 구현할 때, RxSwift를 사용하지 않는 방법도 있습니다. Notification을 사용할 수 있을 것 같기도 하고 didSet, willSet 등을 활용해서 구현할 수 있을 것 같기도 합니다 또는 클로져를 넘겨 구현하는 방법도 있을 것 같습니다.

그러나 View에 View Model을 바인딩하기 좋은 방법을 제공하는 RxSwift을 사용해서 구현해보고 MVVM 패턴을 알아보려고합니다.

 

VIPER Pattern과 동일한 예제를 구현해보면서 알아보겠습니다.

 

 

Practice

VIPER Pattern에서 구현했던 동일한 예시입니다.

간단한 앱으로 TableView가 있고 API를 가짜로 호출하게 한 후, 데이터를 TableView에 뿌려주고 실제 네트워킹을 요청하는 것처럼 로딩을 진행하고 이후 데이터를 다시 화면에 보여주는 로직으로 진행되는 앱입니다.

 

이 역시 객체들의 관계도는 다음과 같이 구성했습니다. VIPER 패턴보다는 간단하고 단조로운 로직을 가지고 있습니다.

 

Model

서버와의 통신으로부터 받아올 데이터로 VIPER 패턴에서 구현되었던 Entity와 같은 역할입니다.

 

별도의 APIService 모듈과 상호작용하여 데이터를 받아오고 화면에 표시합니다. CarDTO는 Car 데이터에서 화면에 보여줄 데이터만을 가공하여 보여주는 객체입니다.

// 서버로 부터 받아오는 데이터
struct Car {
    let id: String
    let make: String
    let model: String
    let trim: String
    
    func makeCarDTO() -> CarDTO {
        return CarDTO(make: self.make,
                      model: self.model)
    }
}

// 화면에 보여줄 데이터
struct CarDTO {
    let make: String
    let model: String
}

 

View Model

View Model 부분에 크게 2가지의 Input, Output을 두어서 사용자의 이벤트에 대응하는 경우는 Input에 할당하고 subscribe를 통해서 화면에 바인딩되어야하는 것들은 Output에서 처리될 수 있게 하였습니다.

/* 
View Model Type
: View Model의 타입을 지정함으로서 어떤 이벤트가 들어오고 나가는지 추상화한 Protocol
*/
protocol ViewModelType {
    associatedtype Input
    associatedtype Output
    
    var input: Input { get }
    var output: Output { get }
}

 

ViewModelType을 채택해 구현한 실제적인 View Model 객체입니다.

 

View ModelView와 1:1로 매칭되게 구현하였고 사용자의 입력을 받거나 사용자에게 보여줄 데이터를 다루는 로직들을 처리한다. 로직만을 다루기 때문에 UIKit과는 완전 별도의 모듈입니다.

 

Input에는 input.reloadTrigger 변수를 사용해서 사용자의 리로드 이벤트를 감지하다가 입력이 들어올 경우 APIService에 요청을 보낼 수 있게 되어 있습니다.

 

Output에는 View에 바인딩하기 위한 변수들입니다. 뷰에 바인딩되어서 처리된다. 즉, View에서 subscribe를 하고 있다가 해당 이벤트가 들어오면 비동기적으로 데이터를 처리합니다.

/*
View Model
: 실제적인 View Model의 객체이다.
: 사용자의 입력을 받아 특정 비즈니스 로직을 처리하거나 API 요청을 보내 데이터를 가져온다.
: 해당 View에서 필요한 전체적인 로직을 처리하는 객체이다. View와 1:1로 매칭했다.
*/
class CarViewModel: ViewModelType {
    struct Input {
        let reloadTrigger = PublishSubject<Void>()
    }
    
    struct Output {
        let carsList = BehaviorRelay<[Car]>(value: [])
        let refreshing = BehaviorSubject<Bool>(value: false)
    }
    
    var input: Input
    var output: Output
    
    let apiService: CarsAPIService
    var disposeBag = DisposeBag()
    
    init(input: Input = Input(),
         output: Output = Output(),
         apiService: CarsAPIService = CarsAPIService(networkManager: NetworkManagerStub())) {
        self.input = input
        self.output = output
        self.apiService = apiService
        
        
        setReloadTrigger()
    }
    
    func fetchCars() {
        apiService.requestCars()
            .subscribe { [weak self] in
                self?.output.carsList.accept($0)
            }
            .disposed(by: disposeBag)
    }
    
    private func setReloadTrigger() {
        self.input.reloadTrigger
            .do(onNext: { [weak self] in self?.output.refreshing.onNext(true)})
            .delay(.seconds(3), scheduler: MainScheduler.instance)
            .flatMapLatest { [weak self] in
                (self?.apiService.requestCars())!
            }
            .do(onNext: { [weak self] _ in self?.output.refreshing.onNext(false) })
            .bind(to: self.output.carsList)
            .disposed(by: disposeBag)
    }
	
    deinit() {
    	disposeBag = DisposeBag()
    }
}

 

View

VIPER 패턴에서와 동일하게 사용자에게 차의 리스트를 보여주는 화면이다. 기본적으로 View Model과 1:1로 매칭이 되며 View Model의 데이터를 바인딩하여 화면에 어떻게 띄울지만 보여주는 모듈입니다.

 

TableView를 설정하거나 사용자의 이벤트에 대한 처리만을 담당하고 있고 모든 비즈니스 로직들은 ViewModel에서 처리하게 구현되었습니다.

 

TableView의 Cell이 View Model의 Output을 바인딩하여서 Cell별로 데이터를 보여주거나 사용자의 입력을 받아 View Model API 요청을 보내거나 등의 사용자 이벤트 및 UI 관련만 처리하고 있습니다.

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    
    let viewModel = CarViewModel()
    var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        setTableView()
        bindTableView()
        
        viewModel.fetchCars()
    }

    func setTableView() {
        tableView.estimatedRowHeight = UITableView.automaticDimension
        
        let refreshControl = UIRefreshControl()
        tableView.refreshControl = refreshControl
        
        refreshControl.rx.controlEvent(.valueChanged)
            .bind(to: viewModel.input.reloadTrigger)
            .disposed(by: disposeBag)
        
        self.viewModel.output.refreshing
            .subscribe(onNext: { [weak self] refreshing in
                if refreshing { return }
                self?.tableView.refreshControl?.endRefreshing()
            })
            .disposed(by: disposeBag)
    }
    
    func bindTableView() {
        viewModel.output.carsList
            .asDriver(onErrorJustReturn: [])
            .drive(tableView.rx.items) { tableView, indexPath, item in
                guard let carCell = tableView.dequeueReusableCell(withIdentifier: CarCell.identifier) as? CarCell else { return UITableViewCell() }
                let carDTO = item.makeCarDTO()
                carCell.configure(from: carDTO)
                return carCell
            }
            .disposed(by: disposeBag)
        
        tableView.rx.itemSelected
            .subscribe(onNext: { [weak self] indexPath in
                guard let carDetailVC = UIComponents.mainStoryboard.instantiateViewController(withIdentifier: DetailCarViewController.identifier) as? DetailCarViewController,
               	let self = self else { return }

                let selectedCar = self.viewModel.output.carsList.value[indexPath.row]
                carDetailVC.viewModel.input.carInform.onNext(selectedCar)

                self.navigationController?.pushViewController(carDetailVC, animated: true)
            })
            .disposed(by: disposeBag)
    }
    
    deinit {
        disposeBag = DisposeBag()
    }
}

 

전체 코드를 참고하고 싶으면 Github를 참고하면 될 것 같습니다.

 

사용해보고 느낀점

구현하면서 느낀 점은 평소 MVC, MVVM 패턴 밖에 사용해본 경험이 없었기에 MVVM이 Unit Test에 적합하다고 느끼고 있었는데 VIPER를 구현해보고 구현하니 또 다른 느낌이었습니다. MVC에 비해 MVVM이 테스트하기 용이하지만 VIPER에 비해서는 조금 약한 구석도 있는 것 같습니다. 하지만 MVVM도 UI와 비즈니스 로직이 분리되어 있기 때문에 Unit Test를 진행하기에는 무리가 없는 것 같습니다.

 

또한 VIPER는 모듈들이 지나치게..(?) 나누어져 있기 때문에 팀 내에서 개발자가 많을 때, 그만큼 역할을 분리하는 것이 화면 단위가 아닌 모듈 단위가 가능하다는 점이 있었습니다. MVVMVIPER 만큼은 아니지만 View Model - View의 들어갈 설계가 완료된 시점에서는 화면 단위가 아닌 모듈 단위로 분업이 가능할 것 같습니다. 그래도 제가 생각하기에는 MVVM은 화면 단위의 분업이 더 적절하다고 느꼈습니다.

 

지금까지 말한 것으로만 보면 MVVM의 장점은 VIPER와 비교되었을 때, 작아보였지만 둘을 비교했을 때 MVVM의 장점은 사용성인 것 같습니다. 아무래도 기존의 MVC에서 약간의 변형으로 구현할 수 있는 패턴이기 때문에 사용하기 용이함은 MVVMVIPER와는 차원이 다르게 편리하다고 생각이 듭니다. 그렇기 때문에 빠른 개발이 필요하고 로직의 분리가 필요할 때는 적절한 패턴인 것 같습니다.

 

최종적으로 느낀 점은 MVVM, VIPER 모두 각자의 장,단점이 있는 것 같습니다. 그리고 이전에는 꼭 한 프로젝트에 하나의 패턴만을 사용해서 통일하는 것이 좋다는 생각이 있었는데 각 화면이나 팀의 상황에 맞게 여러 패턴을 적용해서 작업을 하는 것도 괜찮고 오히려 더 생산성 있고 효율성 있게 개발을 진행할 수 있겠다는 생각이 들었습니다.

 

👉 MVC도 로직이 많이 없고 테스트가 필요없는 화면 단위에는 빠르게 개발하기 적절하다.

👉 MVVM의 경우 ****로직의 테스트가 필요하고 빠른 개발이 필요할 경우 적절하다.

👉 VIPER의 팀 단위가 대규모라 화면 단위로 역할을 나누기 힘든 경우, 앱 구성에 중요한 로직이 많아 테스트가 꼭 필요한 경우에 적절하다.

 

(아래는 각 패턴의 비교가 쉽게 그림으로 표현한 자료입니다.)

 

 

오늘은 MVVM에 대해 알아봤는데, 그냥 MVVM에 대해 알아보기 보다는 VIPER와 비교하면서 느낀점을 바탕으로 작성을 해보았습니다.

혹시 잘못된 점이나 더 좋은 방법이 있으면 말씀해주세요 :)

 

감사합니다

 


참고 레퍼런스

 

MVVM + RxSwift on iOS part 1 [번역] – usinuniverse

MVVM 패턴과 RxSwift에 대해서 알아봅니다.

usinuniverse.bitbucket.io

 

 

 

반응형

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

[Design Pattern] ReactorKit이란?  (0) 2021.10.12
[Design Pattern] VIPER Pattern - iOS Architecture  (4) 2021.02.02