기어가더라도 제대로

[번역] iOS 에서의 클린아키텍쳐와 MVVM 본문

생각정리/번역글(blog, WWDC)

[번역] iOS 에서의 클린아키텍쳐와 MVVM

Damagucci-juice 2022. 9. 14. 22:13

clean architecture + mvvm

Tags: Pattern, Architect, 번역
등록일: 2022.09.12
블로그 포스팅 여부: In progress
작성소요시간: 3:00
학습일: 2022.09.14

가장 기본이 되는 규칙은 안쪽 층에서 바깥층으로 의존성이 없다는 것이다. 오직 안쪽으로만 의존성이 있을 수 있다.

3층 구조, Presentation, Domain, Data

Domain Layer 는 가장 안쪽의 그림이다. 다른 계층으로 의존성도 없고 완전히 분리 되어있다. 도메인은 Entities(비지니스 모델), Use cases, 그리고 Repository Interface 를 가지고 있다. 이 계층은 다른 프로젝트에서 재사용될 수 있다. 이러한 분리는 테스트 타겟과 호스트 앱을 사용하지 않도록 한다. 의존성이 필요하지 않기 때문이다. 이는 도메인의 유즈 케이스 테스트를 수초 내에 실행되도록 한다.

주의: 도메인 계층은 다른 계층으로 부터 아무것도 포함하지 않는다. (프레젠테이션 계층은 UIKit 이나 SwfitUI 를 포함하고 Data 계층은 Codable 을 매핑한다.)

좋은 아키텍처가 유즈 케이스로 둘러 쌓인 이유는 그래야 아키텍쳐가 구조체(그 구조체의 유즈케이스는 프레임워크나 도구나 환경에 얽매이지 않는다.) 를 안전하게 묘사할 수 있기 때문에다. 이를 Screaming Architecture 라고 한다.

Presentation Layer 는 UI(UIViewController or SwiftUI Views) 를 포함한다. 뷰들은 하나 이상의 유즈케이스를 사용하는 뷰모델과 협동한다. 프레젠테이션 계층은 도메인 계층에만 의존한다.

Data Layer 는 Repository 구현체들과 하나 이상의 Data Source 를 포함한다. 레포지토리들은 다른 data source 에서 온 데이터를 협력하는데 책임이 있다. Data Source 는 외부이거나 내부의 Persistant Database 일 수 있다. 데이터 계층은 도메인 계층에만 의존적이다. 이 계층에서 우리는 네트워크의 JSON 데이터를 도메인 모델에 매핑할 수 있다.

아래에 그래프에서 각 계층에서 모든 구성요소는 의존성 방향과 데이터 흐름(요청/응답)으로 표시된다. Repository Interfaces(Protocols)를 사용하는 곳에서 의존성 역전을 볼 수 있다. 각 계층에 대한 설명은 기사 시작 부분에 언급된 예시 프로젝트를 기반으로 합니다.

Data Flow

  1. View(UI) ViewModel(Presenter) 의 메서드를 호출한다.
  2. ViewModel 은 Use Case 를 실행한다.
  3. Use Case 는 User 와 Repository 로 부터 나온 데이터를 결합한다.
  4. 각 Repository는 원격 데이터(Network), Persistent DB 저장소 혹은 인 메모리 데이터(원격 혹은 캐시된)로 부터 온 데이터를 반환합니다.
  5. 아이템의 리스트를 표시하는 View로 정보가 되돌아 갑니다.

Dependency Direction

Presentation Layer → Domain Layer ← Data Repository Layer

Presentation Layer(MVVM) = ViewModels(Presenters) + View(UI)

Domain Layer = Entities + Use Case + Repository Interfaces(Protocols)

Data Repository Layer = Repositories Implementations + API(Network) + Persistence DB

Domain Layer

예제 프로젝트에서 Domain 계층을 찾을 수 있습니다. 도메인 계층엔 엔티티와 영화를 검색하고 최근 성공한 커리를 저장하는 Usecase가 있습니다. 또한 의존성 역전에 필요한 Repositories Interfaces를 포함합니다.

 

: NOTE: Usecase 를 사용하는 다른 방식 하나는 start() 함수를 사용하는 UseCase 라는 프로토콜을 모든 유즈케이스 실행체들이 준수하도록하는 것입니다. 예제 프로젝트 내의 유즈 케이스 중 하나는 이러한 접근을 따릅니다. (FetchRecentMovieQueriesUseCase). 유즈 케이스는 때로 Interactors 라고 불립니다.

주의: Usecase 는 다른 유즈 케이스들에 의존적입니다.

Presentation Layer

프레젠테이션 계층은 MovieListView로 부터 관찰되는 아이템들이 있는 MovieListViewModel을 포함합니다. MovieListViewModel 은 UIKit 을 import 하지 않습니다. UIKit 과 같은 여타 UI Frameworks(SwiftUI, WatchKit)로 부터 뷰모델을 지켜내는 것은 손 쉬운 재사용과 리펙터링을 가능하게 할 것입니다. 예를 들어 미래에 UIKit에서 SwiftUI로 뷰를 리펙터링하는 것이 훨씬 쉬어질 것입니다. 왜냐면 뷰모델이 바뀔 필요가 없으니까요.

 

주의: 뷰모델을 쉽게 목킹하여 MoviesListViewController 를 테스트하기 좋게 하기 위해 MovieListViewModelInputMovieListViewModelOutput 이라는 인터페이스를 사용합니다. 또한 우리는 MoviesListViewModelActions 클로저를 이용해서 언제 다른 뷰를 띄울지에 대해 MoviesSearchFlowCoordinator 에게 전달합니다. 액션 클로저가 요청될 때 코디네이터는 무비의 디테일 스크린을 띄웁니다. 액션들을 그루핑하기 위해 구조체를 사용합니다. 왜냐면 필요에 의해 액션을 더 추가할 수 있기 때문입니다. (output과 Action이 왜 두개가 필요하지? 제 추측에 의하면, output는 observable 과 관련이 있고, action 은 행동과 관련이 있습니다. 둘다 어떤 행동을 유발한다는 공통점이 있지만, observable 은 VC이 이를 관찰하고 뷰를 업데이트 한다면, Action 은 VM에서 다른 어떤 뷰를 띄우라고 지시를 한다던지 하는 행동에 좀더 집중되어 있는 느낌입니다.)

프레젠테이션 레이어는 또한 MoviesListViewModel 의 데이터(아이템)에 대한 책임이 있는 MoviesListViewController 를 포함합니다.

UI 는 비지니스 로직이나 어플리케이션 로직(Business Model, Use Cases)에 접근해서는 안됩니다. 오직 뷰모델만이 비지니스 로직에 접근할 수 있습니다. 이것이 관심의 분리입니다. 직접적으로 비지니스 모델을 뷰(UI)에 전달해서는 안됩니다. 따라서 우리는 뷰모델 내에서 비지니스 모델을 뷰모델로 매핑하고 매핑한 뷰모델을 뷰로 전달합니다.

우리는 또한 영화 검색을 시작하기 위해 검색 이벤트 요청을 뷰에서 뷰모델로 추가합니다.

 

Note: 우리는 아이템을 주시하고 있다가 그들이 변화하면 뷰를 리로드 합니다. 여기서 밑의 MVVM 섹션에서 설명되는 단순한 Observable 을 사용합니다.

우리는 flow coordinator 로 부터 영화의 세부 스크린 정보를 표시하기 위해서 또한 showMovieDetail(movie:) 함수를 MovieSearchFlowCoordinator 내의 MoviesListViewModel 의 액션에 할당합니다.

 

주의: 뷰컨의 크기와 책임을 줄이기 위한 프레젠테이션 로직을 위해 flow coordinator 를 사용합니다. flow 가 필요할 때 메모리에서 해제되지 않게 하도록 Flow 를 강한 참조를 했습니다.

이런 접근으로 우리는 같은 뷰 모델을 가지고 변경이 없이 다른 뷰를 사용할 수 있습니다.

Data Layer

데이터 레이어는 DefaultMoviesRepository 를 가집니다. 이 레포지토리는 도메인 계층 안에 정의된 인터페이스를 준수합니다(의존성 역전). 이곳에 JSON data 와 Core Data 엔티티를 도메인 모델로 매핑을 추가합니다.

 

주의: Data Transfer Object DTO 는 JSON 응답으로 부터 도메인으로에 매핑을 위한 즉각적인 오브젝트로 사용됩니다. 또한 만약 엔드포인트 응답을 캐시하고 싶다면, DTO 를 영구 오브젝트로 매핑하므로써 영구 스토리지 안에 저장할 것입니다. (DTO → NSManagedObject)

일반적인 경우 Data 레포지토리는 API Data Service와 persistent Data storage 와 함께 주입될 수 있습니다. 데이터 레포지토리는 데이터를 반환하기 위해 이 두 가지 의존물과 함께 작업합니다. 규칙은 첫번째로 캐시된 output 데이터가 있는지 영구 저장소에 물어봅니다(NSManagedObject가 DTO를 통해 도메인으로 매핑이되고 이를 cached data 클로저로 반환합니다). 그 후에 가장 최근에 업데이트된 데이터를 반환할 API Data Service를 호출합니다. 영구 저장소가 이 최신 데이터로 업데이트 됩니다.(DTO 들은 영구 객체로 매핑되고 저장됩니다.) 그러고 나서 DTO 는 도메인으로 매핑이 되어서 업데이트 된 데이터 / 컴플리션 클로저로 반환됩니다. 이 방식이 유저들이 즉각적으로 데이터를 보는 방법입니다. 인터넷 연결이 없더라도, 영구 저장소에서 최신 데이터를 볼 수 있습니다.

저장소와 API 는 완전히 다른 수행체로 대체될 수 있습니다(코어 데이터 부터 Realm까지 예를들어). 반면에 앱의 다른 계층들은 이 변화로 영향을 받지 않을 것입니다. 왜냐면 DB가 세부적이거든요.(?)

Infrastructure Layer (Network)

Network 프레임 워크 주변의 wrapper 입니다. Alamorfire(혹은 다른 프레임워크)일 수 있겠네요. 그것은 베이스 URL 같은 네트워크 파라미터로 구성될 수 있습니다. 엔드포인트를 정의하는 데 도움을 주기도 하고 디코더블을 이용해서 데이터 매핑 메소드들을 포함하기도 합니다.

 

MVVM

Model - View - ViewModel 패턴(MVVM) 은 UI 와 도메인 사이의 깔끔한 관심사의 분리를 제공합니다.

클린 아키텍쳐와 함께 사용될 때 프레젠테이션과 UI 계층 사이의 관심사의 분리에 도움을 줄 수 있습니다.

다른 뷰 구현체가 같은 뷰모델과 함께 사용될 수 있습니다. 예를 들어 CarsAroundListView 와 CarsAroundMapView 에 CarsAroundViewModel 을 같이 사용할 수 있습니다. 또한 하나는 UIKit 의 뷰와 다른 SwiftUI 의 뷰를 를 사용할 수 있습니다. 뷰모델에 UIKit, WatchKit, SwiftUI 등을 뷰모델 안에 도입하지 않는 것을 기억하는게 중요합니다. 이 방식은 필요하다면 다른 플랫폼에서 재사용되는 것을 쉽게 하는 방법일 수 있습니다.

뷰와 뷰모델사이에 데이터 바인딩은 예시로 클로저, 델리게이트 혹은 옵저버블(RxSwift)로 완성될 수 있습니다. Combine 과 SwfitUI 을 사용할 수 있지만, 최소 지원되는 시스템이 iOS 13 입니다. 뷰는 뷰모델에 직접적인 관계를 가지고, 뷰 내부에 이벤트가 발생하면 그것을 알립니다. 뷰모델로부터 뷰에 직접적인 참조는 없습니다. (데이터 바인딩만 있습니다.)

이 예제에서 제 3자 의존성을 피하기 위해 간단한 didSet 과 클로저의 조합을 사용했습니다.

 

주의: 이것은 아주 간단한 버전의 옵저버블입니다. 다수의 옵저버블과 옵저버 지우개의 모든 시행을 보고 싶다면

 

편의를 위해 옵저버의 블럭을 메인스레드에서 호출합니다. UI를 포함하는 프레젠테이션 레이어에서 사용되기 때문이죠.

뷰컨트롤러에서 데이터 바인딩 예시입니다.

 

주의: 옵저빙 클로저에서 뷰모델에 접근하는 것은 허용되지 않습니다. 순환 참조(메모리 릭)를 유발하기 때문입니다. 뷰 모델은 self 와 함께 접근되어야합니다.

테이블 뷰 셀에 데이터 바인딩 예시입니다.

 

주의: 뷰가 재사용이 된다면 unbind 를 해야합니다.

MVVM 커뮤니케이션

Delegation

한 MVVM 의 뷰모델은 다른 MVVM의 뷰모델과 Delegation 패턴을 이용해 통신합니다.

예를 들어, ItemListViewModel 과 ItemEditViewModel 이 있습니다. 그 뒤에 ItemEditViewModelDidEditItem 이라는 메서드를 가진 ItemEditViewModelDelegate 프로토콜을 생성합니다. 다른 뷰모델이 이 프로토콜을 채택하도록 합니다.

extension ListItemsViewModel: ItemEditViewModelDelegate

 

주의: 이런 경우 Delegate라는 네이밍 대신 Responders 라는 네이밍을 할 수 있습니다.

ItemEditViewModelResponder

Closures

FlowCoordinator에게 주입되거나 할당되는 클로저를 이용하는 방식의 커뮤니케이션도 있습니다.

예제 프로젝트에서 MoviesListViewModel 이 MoviesQueriesSuggestionsView를 보여주기 위해 액션 클로저(showMovieQueriesSuggestions)를 사용하는 방법을 볼 수 있습니다. 또한 (_ didSelect: MovieQuery) -> Void 파라미터를 넘기므로써 뷰로부터 콜백을 받을 수도 있습니다. 그 의사 전달 과정은 MoviesSearchFlowCoordinator 내부에 연결되어 있습니다.

 

Layer Separation into frameworks(Modules)

이제 예시 프로젝트의 (Domain, presentation, UI, Data, Infrastructure Network) 등의 각 레이어들은 각각의 프레임워크로 쉽게 나눠질 수 있습니다.

New Project -> Create Project… -> Cocoa Touch Framework

그러고 나서 코코아 팟을 이용해 이 프레임 워크들을 메인 앱에 포함할 수 있습니다.

Dependency Injection Container

의존성 주입은 한 객체가 다른 객체의 의존성을 제공하는 기술입니다. 앱 내의 DIContainer는 모든 주입의 중심 단위입니다.

의존성 팩토리 프로토콜을 사용하기

옵션 중에 DIContainer에게 의존성의 생성을 위임하는 의존성 프로토콜을 선언하는 옵션이 있습니다. 이를 위해 MoviesSearchFlowCoordinatorDependencies 라는 프로토콜을 선언해야합니다. 또 MoviesSceneDIContainer 가 이 프로토콜을 채택하도록 해야합니다. 그러고 나서 DIContainer를 MoviesListViewController를 생성하고 나타내기 위해 주입이 필요한 MoviesSearchFlowCoordinator 에게 주입해야합니다. 다음은 그 단계들입니다.

 

클로저 사용하기

다른 옵션은 클로저를 사용하는 것입니다. 이를 위해 우리는 주입이 필요한 클래스 내에 클로저를 선언하고 이 클로저를 주입해야합니다. 예시는 다음과 같습니다.

 

게시글 내 출처

Advanced iOS App Architecture

 

Advanced iOS App Architecture

<h2>Implement Modern Clean Architectures in Your iOS Apps!</h2> <p>Apps are becoming more complex, and development teams are being pressured to deliver faster results in the face of constantly changing requirements. Now, more than ever, you need to underst

www.raywenderlich.com

The Clean Architecture

 

Clean Coder Blog

The Clean Architecture 13 August 2012 Over the last several years we’ve seen a whole range of ideas regarding the architecture of systems. These include: Though these architectures all vary somewhat in their details, they are very similar. They all have

blog.cleancoder.com

The Clean Code

 

Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin) : Martin, Robert: Amazon.de: Books

I am a principal software engineer at Domino Data Lab, an expert in MLOps, and a long-time member of the Scala community. I am also the author of several books: -- "Programming Scala, Third Edition", a practical book for experienced software developers tha

www.amazon.de

( 클린 코드, 클린 아키텍쳐는 꼭 읽어봐야겠습니다…)

결론

모바일 개발 분야에서 가장 흔하게 사용되는 아키텍쳐 패턴은 클린 아키텍쳐와 MVVM, Redux입니다.

MVVM 과 클린 아키텍쳐를 따로 사용할 순 있지만, MVVM은 프레젠테이션 계층에서의 관심사의 분리만을 제공하고, 반대로 클린 아키텍쳐는 쉽게 테스트 되고 재사용되고, 이해될 수 있는 모듈화된 계층으로 코드를 분리합니다.

설령, Use Case를 Reposiotry를 호출 하는 용도로만 쓴다하여도 Use Case의 생성을 해야합니다. 이 아키텍쳐는 신입 개발자가 당신의 Use Case 들을 볼 때 아키텍쳐가 스스로 설명이 될 것입니다.

비록 이것은 시작점으로는 유용해야 하지만, 은총알은 없는 법입니다.(은총알은 서구권에서 주로 비유로 하는데, 드라큘라나 늑대인간을 한방에 해결하는 마법의 무기를 뜻합니다.) 여러분의 프로젝트에서 필요를 수행하는 아키텍쳐를 고르세요.

클린 아키텍쳐는 TDD와 잘 작동합니다. 이 아키텍쳐는 프로젝트를 테스트하기 수월하게 하고 레이어들은 쉽게 대체될 수 있습니다.(UI, Data)

DDD(Domain-Driven Design) 도 클린 아키텍처와 잘 작동합니다.

출처

Clean Architecture and MVVM on iOS

 

Clean Architecture and MVVM on iOS

When we develop software it is important to not only use design patterns, but also architectural patterns. There are many different…

tech.olx.com

 

Comments