관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS] 2. Clean Architecture + MVVM 개념 확실하게 이해하기 (의존 관계 Presentation, Domain, Data) 본문

Architecture (swift)/MVVM (개념)

[iOS] 2. Clean Architecture + MVVM 개념 확실하게 이해하기 (의존 관계 Presentation, Domain, Data)

jake-kim 2021. 8. 18. 00:26

의존관계

  • 잘 변하는 것에서 변하지 않는것으로 의존관계가 되는게 이상적인 형태
  • 잘 변하지 않는 계층인 Domain계층으로 Presentation과 Data 계층이 의존하는 형태
  • 핵심: Actor가 Entity를 확인하는 flow
    • View는 ViewModel의 메소드를 호출
    • viewModel은 useCase 실행 > useCase는 Repository(DB or Network)에 데이터 요청
    • Repository에서 cache에 데이터가 있으면 바로 획득, 없으면 memory cache, disk cache로 기록
    • Respository로 부터 받은 데이터는 completion의 인수로 받을수 있어서 ViewModel이 이 데이터 획득
    • ViewModel은 자신의 Output 프로퍼티에 emit > 이 프로퍼티를 observe하고있던 View에서 Entity를 UI에 표출

Presentation Layer

  • Flow, ViewController, ViewModel이 존재
  • ViewModel은 하나 이상의 Use case를 execute하기 때문에 Domain Layer에 의존

Domain Layer

  • Domain은 Entity, UseCase, Interface 존재

Data Layer

  • DB, Network 존재

  • 주의: Network 기반 구현 로직은 다른 그룹에서 구현, 해당 그룹에서는 Domain에서 사용되는 부분을 구현

Data/Network/ 하위에 있는 코드

Presentation이 Domain에 의존하는 경우

  • ViewModel(Presentation layer)이 Domain에 의존하는 형태
  • ex) MoviesQueryListViewModel은 Movie의 query값을 가지고 movie를 조회하는 usecase `fetchRecentMovieQueriesUseCaseFactory` 존재하고 이 usecase를 통해 Actor가 원하는 MovieQuery값 획득
import Foundation

typealias MoviesQueryListViewModelDidSelectAction = (MovieQuery) -> Void

protocol MoviesQueryListViewModelInput {
    func viewWillAppear()
    func didSelect(item: MoviesQueryListItemViewModel)
}

protocol MoviesQueryListViewModelOutput {
    var items: Observable<[MoviesQueryListItemViewModel]> { get }
}

protocol MoviesQueryListViewModel: MoviesQueryListViewModelInput, MoviesQueryListViewModelOutput { }

typealias FetchRecentMovieQueriesUseCaseFactory = (
    FetchRecentMovieQueriesUseCase.RequestValue,
    @escaping (FetchRecentMovieQueriesUseCase.ResultValue) -> Void
    ) -> UseCase

final class DefaultMoviesQueryListViewModel: MoviesQueryListViewModel {

    private let numberOfQueriesToShow: Int
    private let fetchRecentMovieQueriesUseCaseFactory: FetchRecentMovieQueriesUseCaseFactory
    private let didSelect: MoviesQueryListViewModelDidSelectAction?
    
    // MARK: - OUTPUT
    let items: Observable<[MoviesQueryListItemViewModel]> = Observable([])
    
    init(numberOfQueriesToShow: Int,
         fetchRecentMovieQueriesUseCaseFactory: @escaping FetchRecentMovieQueriesUseCaseFactory,
         didSelect: MoviesQueryListViewModelDidSelectAction? = nil) {
        self.numberOfQueriesToShow = numberOfQueriesToShow
        self.fetchRecentMovieQueriesUseCaseFactory = fetchRecentMovieQueriesUseCaseFactory
        self.didSelect = didSelect
    }
    
    private func updateMoviesQueries() {
        let request = FetchRecentMovieQueriesUseCase.RequestValue(maxCount: numberOfQueriesToShow)
        let completion: (FetchRecentMovieQueriesUseCase.ResultValue) -> Void = { result in
            switch result {
            case .success(let items):
                self.items.value = items.map { $0.query }.map(MoviesQueryListItemViewModel.init)
            case .failure: break
            }
        }
        let useCase = fetchRecentMovieQueriesUseCaseFactory(request, completion)
        useCase.start()
    }
}

// MARK: - INPUT. View event methods
extension DefaultMoviesQueryListViewModel {
        
    func viewWillAppear() {
        updateMoviesQueries()
    }
    
    func didSelect(item: MoviesQueryListItemViewModel) {
        didSelect?(MovieQuery(query: item.query))
    }
}

Data 레이어가 Domain에 의존하는 경우

  • Domain에 UseCase가 Data의 Repository Protocol에 의존하지만, Data의 구현체가 Protocol을 의존하고 있는 형태 (DIP)
  • 위 예제 코드에서 사용되는 `FetchRecentMovieQueriesUseCase`에서 repository에 접근하는 형태
// This is another option to create Use Case using more generic way
final class FetchRecentMovieQueriesUseCase: UseCase {

    struct RequestValue {
        let maxCount: Int
    }
    typealias ResultValue = (Result<[MovieQuery], Error>)

    private let requestValue: RequestValue
    private let completion: (ResultValue) -> Void
    private let moviesQueriesRepository: MoviesQueriesRepository

    init(requestValue: RequestValue,
         completion: @escaping (ResultValue) -> Void,
         moviesQueriesRepository: MoviesQueriesRepository) {

        self.requestValue = requestValue
        self.completion = completion
        self.moviesQueriesRepository = moviesQueriesRepository
    }
    
    func start() -> Cancellable? {

        moviesQueriesRepository.fetchRecentsQueries(maxCount: requestValue.maxCount, completion: completion)
        return nil
    }
}
  • `FetchRecentMovieQueriesUseCase`의 구현체는 Data 레이어에서 구현된 것
  • ex) DIContainer에서 주입
// MoviesSceneDIContainer

    func makeFetchRecentMovieQueriesUseCase(requestValue: FetchRecentMovieQueriesUseCase.RequestValue,
                                            completion: @escaping (FetchRecentMovieQueriesUseCase.ResultValue) -> Void) -> UseCase {
        return FetchRecentMovieQueriesUseCase(requestValue: requestValue,
                                              completion: completion,
                                              moviesQueriesRepository: makeMoviesQueriesRepository()
        )
    }
    
    func makeMoviesQueriesRepository() -> MoviesQueriesRepository {
        return DefaultMoviesQueriesRepository(dataTransferService: dependencies.apiDataTransferService,
                                              moviesQueriesPersistentStorage: moviesQueriesStorage)
    }

구현체가 Data 그룹 하위에 존재

* .drawio 파일:

cleanArchiteecture.drawio
0.00MB

* 참고

- https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Comments