Skip to content

모모의 Rx Clean Architecture 도입기

DOHYUN CHUNG edited this page Sep 18, 2022 · 7 revisions

MOMO의 기존 아키텍처, MVC

모모의 기존 아키텍처는 MVC 패턴이다. MVC의 장점으로 생각해본다면, 아무래도 가장 흔히 아는 아키텍처라는 것이다. 그래서 누군가 와도 MVC 아키텍처를 보면 바로 어떤 기능이 어디 있겠구니 라는 생각을 대강 할 수 있다. 그만큼 단순한 패턴인 것 같다. 하지만, 기능이 많아질수록, 어떤 기능이 대강 있다는 것을 판단하는 것이 도움이 되지 않았다. 개발을 하면서, ViewController에 코드가 500줄이 넘어가면서, 뷰와 로직을 나눠야한다는 필요성이 생겼다. 물론, 앱 내에서 특정 기능들은 Service나 Manager 객체로 나눠두긴 했다. 실제로 MARK로 단위를 나눠두더라도, 하나의 Viewcontroller에서 기능 수정을 할 때, Ctrl + F 가 많이 쓰였다.

MVVM의 도입

모모의 처음으로 변경은 MVC에서 MVVM으로 변경하는 것이었다. MVVM으로 변경하면서, View 관련 코드과 그렇지 않는 코드를 분리하는 작업을 진행했다. 아래의 작업은 PolicyMainViewController 내부에 있는 네트워크 통신하는 코드이다. 이러한 View와 관련 없는 코드를 새로운 곳으로 옮기는 작업을 진행했다. 사실 하나의 뷰에서 networking 작업이 여럿 있을경우, 아래와 같은 코드들이 하나의 ViewController에 담기게 된다. 따라서 이러한 코드를 ViewModel로 옮기고, 데이터가 ViewModel에서 View로 바인딩 되었을때, 해당 데이터에 맞춰서 UI를 변경하도록 한다.

private func getPolicyData(category: String, page: Int) {
    let category = category.components(separatedBy: " ").last!
    guard let token = UserManager.shared.token else {return}
    var location: String? = locationTextField.text
    if locationTextField.text == "전국" { // 전국이면 전체를 다 빼준다.
        location = nil
    }
    networkManager.request(apiModel: GetApi.policyGet(token: token, keyword: searchField.text, location: location, category: Filter.getCase(korean: category), page: page)) { (result) in
      switch result {
      case .success(let data):
        let parsingManager = ParsingManager()
        parsingManager.judgeGenericResponse(data: data, model: [PolicyData].self) { [weak self] (body) in
          guard let self = self else {return}
          DispatchQueue.main.async {
             // 중략 (대략적으로 tableView변경 코드)
          }
        }
      case .failure(let error):
        DispatchQueue.main.async { [weak self] in
          if error as! NetworkError == NetworkError.failResponse {
            // 중략 (오류가 발생했을때의 UI 변경코드)
        }
      }
    }
  }
}

Clean Architecture + Input&Output 패턴 도입

하지만 이러한 코드를 옮겨도, MVVM이 방대해지는 문제가 발생했다. 특히 하나의 View에서 여러 네트워크 통신을 하는 부분에서는 크게 발생했다. 그렇기에, 네트워크 통신, 데이터 정제, ViewModel, View가 레이어로 나눠져있는 Clean Architecture를 사용하기로 했다. 또한 Viewmodel에서 View로 데이터 바인딩하는 방식으로는 Input & Output 패턴을 사용하도록 했다. 그리고 서로의 비동기 데이터 바인딩이 편리하도록, RxSwift를 사용하기로 했다.

Input&Output Pattern

Input&Output 패턴은 View와 ViewModel 사이의 data binding 하는 패턴 중 하나이다. 이 패턴을 적용한 이유는 협업을 위해서였다. MVVM 아키텍처를 고수하는 다양한 앱, 코드는 굉장히 많았고, 그에 대해서 어떻게 기준을 잡아야지 코드의 일관성을 유지할까?를 생각하던 중, Input&Output 패턴은 유용했다. MVVM의 간단한 메커니즘은 View에서 Viewmodel에 데이터를 요청하면 ViewModel에서는 View로 해당 데이터를 바인딩한다.

CalendarViewModel.swift의 Input&Output

// MARK: - Input
  struct Input {
    var didSelectCell: AnyObserver<(IndexPath, Day)>
  }
  
  var input: Input
  
  // MARK: - Output
  struct Output {
    var days: Driver<[Day]>
    var numberOfWeeksInBaseDate: Driver<Int>
    var closeView: Driver<Void>
    var calendarHeaderViewModel: CalendarHeaderViewModel
    var diaryInputOptionViewModel: Driver<DiaryInputOptionViewModel>
    var readDiaryViewModel: Driver<ReadDiaryViewModel>
    var toastMessage: Driver<String>
  }
  
  var output: Output

Repository Pattern 적용하기

리포지토리 패턴은 데이터의 추상화를 제공하여, 어플리케이션이 추상화된 인터페이스로 작업할 수 있게 해준다. 리포지토리 패턴을 사용하면서, 데이터의 출처와 관계없이 동일한 인터페이스로 데이터를 사용할 수 있게 해주는데, 이 패턴의 장점은 ViewModel, 만약 Clean Architecture 라면 Usecase가 리포지토리 객체를 주입받을때의 데이터 형이 protocol이기 때문에, viewModel이나 Usecase에서 구현체를 확인할 수 없다. 이를 활용해서 Testable하게 코드를 작성할 수 있게 된다. RecommendReposity 프로토콜을 채택하면서, 필수 메소드를 구현한다.

class MomoRecommendRepository: RecommendRepository {
  
  //MARK: - Private properties
  private let remoteAPI: RecommendRemoteAPI
  private let userSessionDataStore: UserSessionDataStore
  
  //MARK: - init
  init(remoteAPI: RecommendRemoteAPI, userSessionDataStore: UserSessionDataStore) {
    self.remoteAPI = remoteAPI
    self.userSessionDataStore = userSessionDataStore
  }

}

사용하는 객체에서는 프로토콜로 변수형을 쓴다.

private let repository: RecommendRepository

이렇게 하면서, RecommendRepository을 채택한 Test객체를 활용하여, test를 할 수 있다.

Clean Architecture 도입 시작

클린아키텍처의 적용은 현재진행중이지만, 클린아키텍처를 적용하면서 만났던 문제점, 트러블 슈팅은 현재 정리중에 있다.

  1. Usecase를 어떻게 해야하지?