본문 바로가기
Architectures

[Architectures] MVVM + Clean Architecture (feat. ReactorKit)

by Jiseong 2022. 7. 26.

거진 몇달째 MVVM + Clean Architecture를 사용하였지만, 아키텍쳐에 대해서 글을 써본 적이 없는 것 같아서..

 

부족하지만 누군가 한명에게라도 도움이 된다면 좋으니깐 ㅋㅋㅋ~~~~~

 

그리고 나도 뭐 이론적인 부분을 더 채워넣기위해 이번엔 MVVM + Clean Architecture에 대해서 알아보려한다.

우선 Clean Architecture가 뭥미

우린 재사용성, 확장성, 구조적 용이, 테스트 용이, 코드 가독성, 모듈화 등등의 많은 이유로 특정 프로젝트에 많은 아키텍쳐를 시도하고 도입한다.

 

그 중 Clean Architecture는 무엇이냐..? 에 대한 설명은

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

 

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

여기에 더 잘 되어있긴한데.. 그래도 정리해봐야 되니깐 ^^ 화띵^^

 

먼저 그 유명한 엉클밥 센세이께서 클린 아키텍쳐를 만든 이유는 무엇이냐?

 

엉클밥 센세이께서는 수많은 아키텍쳐를 창시하셨지만, 그 아키텍쳐들의 궁극적인 목표는 같았다.


1. Independent of Frameworks. The architecture does not depend on the existence of some library of feature laden software. This allows you to use such frameworks as tools, rather than having to cram your system into their limited constraints.
- 아키텍처는 소프트웨어 라이브러리 존재 여부에 의존하지 않는다.

2. Testable. The business rules can be tested without the UI, Database, Web Server, or any other external element.
- 비즈니스 규칙은 UI, 데이터베이스, 웹 서버, 기타 외부 요인없이 테스트가 가능하다.

3. Independent of UI. The UI can change easily, without changing the rest of the system. A Web UI could be replaced with a console UI, for example, without changing the business rules.
- 시스템의 나머지 부분을 변경할 필요 없이 UI를 쉽게 변경할 수 있다.

4. Independent of Database. You can swap out Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.
- 비즈니스 규칙은 데이터베이스에 얽매이지(바인딩되지) 않는다.

5. Independent of any external agency. In fact your business rules simply don’t know anything at all about the outside world.
- 비즈니스 로직은 외부 세계에 대해 전혀 알지 못한다.

 


이러한 아키텍처를 단일 개념으로 통합하고자 만들어진 아키텍쳐가 바로 Clean Architecture이다.

 

더 자세히 알기전에 클린 아키텍쳐에서 가장 중시하는 Dependency Rule을 먼저 알고 넘어가자~~

https://github.com/kudoleh/iOS-Clean-Architecture-MVVM

※ 프로젝트는 저 프로젝트로 예를 들게 아니라서 내 프로젝트로 이미지도 짜집기했다. 내가 예시로 할 프로젝트의 구조는 다음과 같다.

 

클린 아키텍쳐에서 중시하는 Dependency Rule은 다음과 같다.

  1. 위의 원에서 보이는 룰의 화살표처럼 안쪽을 향해서만 의존할 수 있다는 것
  2. 안쪽 레이어는 바깥쪽 레이어에 위치한 것들을 몰라야 한다는 것
  3. 특히, 바깥쪽의 원에서 선언된 어떠한 것도 안쪽 원에서 참조해서는 안된다는 것

그냥 안쪽 레이어는 바깥쪽의 레이어를 의존하면 안된다라는거임, 의존을 안하면 바깥쪽 놈이 안쪽 놈에 영향을 끼치지 않겠지? 안쪽 놈은 바깥쪽 놈을 모르니깐 ㅇㅇ

 

그럼 이제 각각의 레이어에 대해서 알아보자

Domain, Presentation, Data Layer

이렇게 세 부분의 레이어로 나눠지는데, 각각 레이어를 알아보자

 

먼저 원에서도 가장 안쪽에 위치한 Domain Layer

 

위에서 설명했듯이 안쪽 레이어는 바깥쪽 레이어를 몰라야된다고 했다.

 

그 말인 즉슨 Domain Layer는 아무것도 모르는 독립적인 상태여야만 한다는 것이다.

 

아니 그럼 Domain Layer는 아무것도 모르면 지 혼자 뭐함? 이라고 생각이 들 수 있다. 나도 그랬음. 그림 보고 이해가 안됐음 그냥

 

우선 Domain Layer라는 놈이 무엇을 갖고 있는지부터 보자

;; 좀 개선해보고싶은 게 있어서 M인데 불편하다.

이렇게 Entity, UseCase, Interface가 위치하고 있다.

Entity?

Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.

이게 클린 아키텍쳐 글에 써있는 Entity에 대한 설명인데 걍 개어렵게 설명되어 있다. 뭔 소린지 모르겠음

 

그냥 Entity 리모트나 로컬에서 받아오는 원본 데이터로 생각하면 된다. 뭐 상황에따라 뷰나, 비즈니스 로직에 쓰일 데이터(비즈니스 모델)로도 쓰일 수 있겠지? 필요한것만 원본에서 뽑아쓰면 되니까

 

UseCase는 무엇이냐?

UseCase비즈니스 로직이 위치한 곳이다. 또한 Repository의 Interface를 가진다.

import Foundation

import RxSwift

final class HomeUseCase: HomeUseCaseType {
    //MARK: - Properties

    private let productRepository: NetworkRepository
    private let favoriteProductRepository: CoreDataRepository
    
    //MARK: - Init

    init(
        productRepository: NetworkRepository = ProductRepository(),
        favoriteProductRepository: CoreDataRepository = FavoriteProductRepository()
    ) {
        self.productRepository = productRepository
        self.favoriteProductRepository = favoriteProductRepository
    }
    
    ...
 }

 

바깥 레이어로부터 내려온 데이터를 앱의 비즈니스 로직을 통해 조합을 하는 등의 동작을 위치시키고, Entity에 영향을 준다.

 

보통의 MVVM은 비즈니스 로직이 ViewModel에 위치하고 있지만, CleanArchitecture에서의 ViewModel은 오로지 뷰가 보낸 특정 이벤트에 대한 응답 로직을 위치시키고, 비즈니스 로직은 Domain Layer로 분리시킨다.

UseCase 고찰 

당연히 현업에선 차원이 다르겠지만 보통의 간단한 프로젝트에선 UseCase에 도대체 뭘 둬야될지 모를 경우가 있을 것이다.

 

스스로 하는 간단한 프로젝트에선 API를 통해 받아온 데이터를 매핑하여 뷰에 그려주는 경우가 다 인것이 다반사니깐..

 

그런 상황에서 UseCase의 모습을 보면 그냥 뭐 Repository랑 ViewModel(Reactor)를 연결하는 다리의 역할밖에 하지않는 것을 종종 볼 수 있다.

 

괜히 파일만 쓰잘데기없이 많아지고 뭐 하는 것도 없는 애가 중간에 껴있으니 거슬릴 수 밖에 없다.

 

그럴 때는 뭐.. 지워버려도 된다고 생각한다. 필요없는게 맞으니깐

 

하지만 앱에 특별한 비즈니스 로직이 생길 경우엔 기존에 그냥 하는 것 없이 다리 역할만 하던 놈한테 특정 로직이 담긴 메서드와 필요한 데이터를 내려주는 Repository 인터페이스만 껴넣어주면 되니 향후 확장성엔 용이할 수도 있지않을까? 란 생각이 든다. 

 

지금 내가 예로 들고있는 프로젝트의 UseCase 또한 별걸 안한다.

 

하지만 예를들어 리모트와 서버에서 내려오는 데이터를 매핑하여 뷰모델로 내려줘야 할 경우 해당 로직이 기존엔 뷰모델에 담겨있을텐데, 이를 역할을 분명히 나눠 분리해주면 뷰모델의 테스트는 물론이고, 각각의 레이어의 역할이 분명히 나뉘어 다른 놈들까지 테스트, 확장성이 용이해진다.

 

내가 예로 들고 있는 프로젝트의 UseCase의 메서드를 하나 보면..

func fetchProducts(page: Int) -> Observable<[Product]> {
     return Observable.zip(
         productRepository.fetchProducts(page: page),
         favoriteProductRepository.fetchFavoriteProduct()
     )
     .map { products, favoriteProducts in
         let favoriteProductsId = favoriteProducts.map { $0.id } // 로컬에 저장된 찜한 프로덕트의 id값과 서버에서 내려오는 프로덕트들의 id값이 포함되어있을시 isfavorite true로 변환하여 내려주는 것
            
         return products.map {
             return Product(
                 id: $0.id,
                 name: $0.name,
                 thumbnailPath: $0.thumbnailPath,
                 descriptionImagePath: $0.descriptionImagePath,
                 descriptionSubject: $0.descriptionSubject,
                 price: $0.price,
                 rate: $0.rate,
                 isFavorite: favoriteProductsId.contains($0.id),
                 favoriteRegistrationTime: $0.favoriteRegistrationTime
             )
         }
     }
 }

서버에서 내려오는 데이터(원본)와 리모트에서 내려오는 데이터(사용자가 찜한 데이터 목록)를 묶어 "서버에서 내려오는 놈들 중에 얘랑 애는 사용자가 찜해놓은거니깐 상태값 바꿔" 하며 특정 모델을 만들어내는 비즈니스 로직이 위치하고 있다.

Interface?

얘는 뭐.. 위에서 여러번 말했으니 알겠지만, Domain Layer는 외부 레이어를 몰라야한다.

 

특정 Layer에 의존하지 않기위해 특정 Layer를 추상화한다. 

 

DI(Dependency Inversion)을 위한 Repository Interface이다.

 

나머지 레이어들은.. 간단하다.

Presentation Layer

이름 막 지은거

뷰와 뷰를 위한 로직(뷰모델)이 위치한 친구들은 Presentation Layer로 둔다.

 

각 View는 하나 이상의 Use Case를 실행시키는 ViewModel(Reactor)와 매칭된다.

 

하나 이상의 UseCase가 필요한 예로는 특정 뷰의 여러 종류의 로직들을 통해 나온 데이터가 필요할 경우겠지?

 

위의 과녁 모양 그래프에서도 봤듯이, 도메인 레이어에만 의존하고 있다.

Data Layer

Domain Layer에선 Repository의 인터페이스를 갖고있었고, Data Layer는 Repository의 Implementation을 갖고있다.

 

또한 Repository에서 사용할 DataSource를 갖고 있고, DataSource는 리모트가 될 수 있고, 로컬이 될 수도 있다.

 

여기서 헷갈리지 말아야 할 것은 위의 원에서는 안쪽의 Presenter, 바깥의 API, DB, UI 등이 있는데 이 Presenter가 Presentation layer에 대응되고 가장 바깥의 영역이 Data Layer인 것이 아니다.

X 표시한게 틀렸단 게 아님, 이 그림에선 내가 그린 화살표를 보란 것임

또한 Data layer에는 네트워크에게 원본 데이터(DTO) 를 도메인 모델로 매핑하는 것을 포함시킬 수 있다.

extension Response.ProductDataResponse.ProductResponse {
    struct ProductDescriptionResponse: Decodable {
        let imagePath: String
        let subject: String
        let price: Int
    }
    
    func toDomain() -> Product {
        return Product(
            id: id,
            name: name,
            thumbnailPath: thumbnailPath,
            descriptionImagePath: description.imagePath,
            descriptionSubject: description.subject,
            price: description.price,
            rate: rate,
            isFavorite: false,
            favoriteRegistrationTime: nil
        )
    }
}

이런식으로 

Data Flow

흐름은 아래와 같다.

  1. View에서 발생한 이벤트를 ViewModel(Reactor)에 전달
  2. ViewModel이 의존하고 있는 UseCase에게 이벤트에 따른 특정 동작 요청
  3. UseCase가 의존하고 있는 Repository에게 이벤트에 따른 특정 동작 요청
  4. Reposiory가 의존하고있는 DataSource에게 이벤트에 따른 동작 요청
  5. DataSource에게 요청한 것에 대한 응답(CallBack)데이터 반환
  6. Repository는 DataSource로부터 반환된 데이터를 매핑하여(안해도 됨) UseCase로 반환
  7. 데이터를 특정 비즈니스 로직을 통해 뷰에게 필요한 데이터로 매핑하여 ViewModel로 반환
  8. 반환받은 데이터를 View에게 전달 (반응형 프로그래밍일 경우 ViewModel이 가진 데이터를 뷰가 Subscribe하여 이벤트 관찰)

Dependency Injection

 

Presentation Layer ➡️ Domain Layer ⬅️ Data Repository Layer

  • Presentation layer(MVVM, ReactorKit) - ViewModel(Presenter) + View(UI)
  • Domain layer - Entity + UseCase + Repositories Interface
  • Data layer = Repository Implementation + API + Local

UseCase와 Repository사이에 있는 Dependency Inversion에 유의하자

뭐가 좋았을까?

역할 분리가 명확하고 기능을 추가하거나 개선할 때 특정 레이어만 접근하기 때문에 확장성과 개발 편의성이 좋다고 생각했다.

 

또한 역할 분리가 명확히 나뉘어 있기 때문에 테스트와 코드를 파악하는데 이점이 있다고 생각하였고, 레이어의 요소를 추상화하여(UseCase, Repository, Service)테스트를 진행했음

 

그 결과.. 

커버리지는 마약이다.

 

난 ReactorKit을 쓰긴했는데 뭐.. 비슷하니깐 ㅇㅇ ReacotKit은 담에 다뤄보는 걸로~~~

 

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

 

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

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

 

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

https://github.com/yim2627/left7/blob/master/README.md

 

GitHub - yim2627/left7: ReactorKit + Unit Test

ReactorKit + Unit Test. Contribute to yim2627/left7 development by creating an account on GitHub.

github.com

 

댓글