Data Repository Layer in iOS MVVM

윤형찬·2021년 6월 30일
0

iOS Design Pattern

목록 보기
1/1
post-thumbnail


iOS 앱 개발에서는 API, Database 및 기타 데이터 소스(IoT 디바이스 데이터)에서 다양한 유형의 데이터를 가져와야 합니다.

점점 더 많은 개발자가 MVVM 아키텍처를 적용하기 시작하면서 데이터 계층이 체계적이지 않은 것 처럼 보입니다.
Codebase가 구축될 때, View Model(이하 VM)은 다양한 데이터 유형에 대한 풍부한 데이터 소스 클라이언트를 유지해야 합니다.

이 글에서, third party 데이터를 올바르게 처리하는 방법에 대해 알아보기 위해 MVVM 아키텍처의 Data Repository Layer(데이터 저장소 계층)에 대해 설명합니다.

앱의 요구사항은 다음과 같습니다.

  • 앱은 인터넷에 연결되어 있을 때 API에서 데이터를 가져와야하고
  • 인터넷이 없을 때는 데이터베이스에서 데이터를 검색해야 합니다.

1. Overall Structure

아래 그래프에서 볼 수 있듯이 다음과 같습니다.

  • View: UIView(V) + ViewController(VC.
    V VC 소유한 모든 static view 코드가 있습니다. VC는 V 및 VM을 Coordinate합니다.
  • VM: Observable value를 VC에 binding하고, exposing(노출)하는 모든 logic을 처리합니다. VM에서 업데이트된 내용이 VC에 자동으로 표시가 됩니다.
  • Data Repository: 모든 데이터베이스, 네트워킹 및 기타 데이터 처리 logic을 마무리(wrap up)합니다.

2. Data Repository Layer

import Foundation

// 1
protocol DataRepositoryProtocol {
   func initFetch(complete completion: @escaping (Result<[Meteorite], APIError>) -> Void)
   

class DataRepository { 
   
   var fetchData: (() -> ())?
   // 2
   private let apiClient: APIClient
   // 3
   private var dbContainer: Container?
   
   init() {
      // 4
      self.apiClient = APIClient()
      do {
         self.dbContainer = try Container()
      } catch let error { 
         Global.printToConsole(message: error.localizedDescription)
      }
   }
   // 5
   private func saveToDB(_ meteorites: [APIMeteorite]) {
      do {
         try dbContainer?.write { transaction in 
            //TODO: Too much CPU, 13% increased
            meteorites.forEach { item in 
               transaction.add(item, update: .modified)
            }
         }
      } catch (let error) {
          Global.printToConsole(message: error.localizedDescription)
      }
   }
   // 5
       private func getDbInfo(complete completion: @escaping (Result<[Meteorite], APIError>) -> Void) {
        
        let results = dbContainer?.values(
           APIMeteorite.self,
           matching:nil
        )
        completion(.success(results?.filter { $0.geolocation != nil } ?? []))
    }
}

extension DataRepository: DataRepositoryProtocol {
   // 6
   func initFetch(complete completion: @escaping (Result<[Meteorite], APIError>) -> Void) {
       apiClient.getListInfo(from: .listRecords) { [weak self] result in
           switch result {
           
           case .success(let meteorites):
              completion(.success(meteorites))
              DispatchQueue.main.async {
                  self?.saveToDB(meteorites)
              }
              
          case .failure(let error):
              completion(.failure(error))
              DispatchQueue.main.async {
                  self?.getDBInfo { result in
                      completion(.success( try! result.get()))
              }
          }
       }
   }
}
   
  1. View Layer에 제공되는 프로토콜입니다.
  2. apiClient는 모든 네트워킹 요청을 처리하는 네트워킹 클라이언트입니다.
  3. 데이터베이스 Container에는 DB 작업이 포함됩니다.
  4. init() 에서는 네트워킹 클라이언트, DB 컨테이너 및 기타 데이터 소스를 초기화 합니다.
  5. 이 함수는 데이터 가져왹 및 저장 작업을 처리하는 private DB function 입니다
  6. VM layer에 표시되는 프로토콜 function입니다.
    기본적으로 함수는 API를 먼저 호출합니다. 데이터를 성공적으로 가져오면 데이터가 전송되고, 그렇지 않으면 데이터베이스에서 검색되어 VM계층으로 다시 전송됩니다.

Data Repository Layer를 추가할 때의 이점은 다음과 같습니다.

  • Login 숨기기: 데이터를 가져오는 로직이 상위 계층에서 숨겨집니다. VM은 request를 초기화 하므로 데이터의 출처에 대해서는 신경 쓰지 않아도 됩니다.
  • Test하기 쉬움
  • Update하기 쉬움: ORM 모델 계층을 업데이트하거나 로직을 업데이트 하려는 경우 데이터 data repository protocol이 변경되지 않으므로 VM 로직을 변경할 필요가 없다.

3. ViewModel Layer

import Foundation

final class MeteoriteViewModel {
    // 1
    private var dataRepo: DataRepositoryProtocol
    var meteoriteList = [Meteorite]()
    // 2
    private var cellViewModels: [MeteoriteListCellViewModel] = [MeteoriteListCellViewModel]() {
        didSet {
            self.reloadTableViewClosure?()
        }
    }
    // 2
    var isLoading: Bool = false {
        didSet {
            self.updateLoadingStatus?()
        }
    }
    // 2
    var alertMessage: String? {
        didSet {
            self.showAlertClosure?()
        }
    }
    
    var numberOfCells: Int {
        return cellViewModels.count
    }
    var isSegueAllowed: Bool = false
    var selectedMeteorite: Meteorite?
    // 2
    var reloadTableViewClosure: (()->())?
    // 2
    var showAlertClosure: (()->())?
    // 2
    var updateLoadingStatus: (()->())?
    let sizeAbsence = Double(APINULL.noSize.rawValue)
    // 3
    init(dataRepo:DataRepositoryProtocol = DataRepository()) {
        self.dataRepo = dataRepo
    }
    // 4
    func initFetch() {
        self.isLoading = true
        
        dataRepo.initFetch{ [weak self] result in
            self?.isLoading = false
            
            switch result{
            case .success(let meteorites):
                self?.processMeteoriteToCellModel(meteorites: meteorites)
            case .failure(let error):
                self?.processError(error: error)
            }
        }
    }
  1. dataRepo: DataRepositoryProtocol이 제공되므로, 이 VM에서는 initFetch() 기능만 표시된다.
  2. 2번을 모두 사용하면, layer에 바인딩할 수 있습니다. 변수 값이 변경되면 일부 View 작업을 실행하기 위해 View Layer에 바인딩된 didSet() 클로저를 호출할 것입니다. 또한 RxSwift BehaviorRelay와 Combine을 사용하여 이러한 코드를 교체할 수 있습니다.
  3. 이는 VM에 데이터 저장소를 주입하는 일반적인 DI입니다. 이를 통해 데이터 layer와 VM layer를 분리했습니다. 로직 function을 테스트하기가 더 쉽습니다.
  4. initFetch 기능은 VC에 노출되며, 이 기능에서는 데이터를 가져오고 처리하기 위해 DataRepositoryProtocol의 initFetch()를 호출합니다.

4. Conclusion

이 설계 패턴을 이해하면, 개발자가 복잡한 세부 정보를 숨기고 균일한 인터페이스를 외부에 노출하기 위해 항상 사용하는 Facade Pattern이라는 것을 알 수 있습니다.

logic에 필요한 Data만 전송하므로 Codeable Model에서 데이터에 액세스 하는 것보다 MVVM과 더 잘 일치합니다.


source : https://github.com/hyengchan/MeteoriteRecordApp
번역: https://blog.devgenius.io/data-repository-layer-in-ios-mvvm-562541b46f91

profile
https://github.com/velmash

0개의 댓글