천원의 개발

Swift Clean Architecture 정리 본문

iOS&Swift🍎/Swift

Swift Clean Architecture 정리

천 원 2023. 11. 23. 15:33

안녕하세요. 천원입니다.

오늘은 UIKit과 함께 많이 사용되는 시스템 아키텍쳐인 Clean Architecture를 정리하고자 이 글을 작성합니다.

 

소프트웨어 개발 방법론 중 에자일의 창시자인 로버트 C.마틴 선생님이 작성하신 Clean Architecture는 그동안의 디테일만 다르고 유사한 목적을 가지는 다양한 아키텍쳐를 통합하기 위해 고안한 아키텍쳐 패턴입니다. 그동안의 아키텍쳐들은 모두 소프트웨어를 계층으로 나눠서 관심사를 분리하였는데 이렇게 하면 만들어지는 소프트웨어는 아래와 같은 특징을 가집니다.

 

1. Independent of Frameworks: 아키텍쳐는 소프트웨어 라이브러리의 존재에 의존하지 않는다. 이를 통해 시스템을 제한된 제약 조건에 끼워 넣을 필요 없이 프레임워크를 도구로 사용할 수 있다.

2. Testable: 비지니스 로직은 UI, Database, Web Server 등이 없이도 독립적으로 테스트가 가능하다.

3. Independent UI: UI는 system과 비지니스 로직에 관계없이 쉽게 변경이 가능해야 합니다.

4. Independent of Database: 데이터 베이스를 Ocrale -> SQL Server -> Mongo DB 이렇게 변경을 해도 비지니스 로직은 DB에 바인딩 되지 않으니 상관이 없습니다.

5. Independent of any external agency: 비지니스 로직은 외부 세계에 대하여 전혀 알지 못합니다.

 

The Dependency Rule

소프트웨어 개발을 학습하신 여러분이라면 한번쯤은 접해봤을 Clean Architecture의 다이어그램 입니다. 복잡해 보이지만 사실 별거 아닌 이 다이어그램을 한번 살펴보겠습니다. 먼저 가장 중요한 개념이 The Dependency Rule인데 정말 간단합니다. 내부에 레이어는 외부의 레이어에 의존성을 띄면 안됩니다. 더더 간단하게 말하자면 Entities 레이어는 Use Cases 레이어에 존재를 몰라야 합니다. 그럼 각 레이어는 어떤 애들을 지칭하는지 저희는 iOS 개발자이니 Swift와 함께 살펴보겠습니다. Clean Architecture로 작성된 대표적인 Swift 프로젝트 인데 영화를 검색하여 정보를 얻는 이 프로젝트를 예로 들어 진행하겠습니다. 

 

Entitiy

가장 안쪽에 있는 Entitiy 레이어는 모든 레이어에서 의존성을 가지고 있을테니 변화가 가장 적어야 하고, 바깥의 애들은 아무것도 몰라야 하는 애들입니다. 위의 프로젝트를 예로 들자면 바로 Movie라는 데이터 구조가 Entitiy입니다.

struct Movie: Equatable, Identifiable {
    typealias Identifier = String
    enum Genre {
        case adventure
        case scienceFiction
    }
    let id: Identifier
    let title: String?
    let genre: Genre?
    let posterPath: String?
    let overview: String?
    let releaseDate: Date?
}

struct MoviesPage: Equatable {
    let page: Int
    let totalPages: Int
    let movies: [Movie]
}

 

Use Case

이 계층은 엔티티와의 데이터 흐름을 조정하고, 해당 엔티티가 사용자의 목적을 달성할 수 있도록 하는 비지니스 로직이 포함되어 있습니다. 쉽게 말하자면 Movie 앱에서 Movie를 보여주기 위해 Entitiy를 가공할 비지니스 로직이 존재하는 레이어입니다. 이 계층은 DB나 UI의 변경으로 인해 영향을 받아서는 안됩니다. 위의 Movie 앱에서 Use Case 구조를 가져오자면 

final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {

    private let moviesRepository: MoviesRepository
    private let moviesQueriesRepository: MoviesQueriesRepository

    init(
        moviesRepository: MoviesRepository,
        moviesQueriesRepository: MoviesQueriesRepository
    ) {

        self.moviesRepository = moviesRepository
        self.moviesQueriesRepository = moviesQueriesRepository
    }

    func execute(
        requestValue: SearchMoviesUseCaseRequestValue,
        cached: @escaping (MoviesPage) -> Void,
        completion: @escaping (Result<MoviesPage, Error>) -> Void
    ) -> Cancellable? {

        return moviesRepository.fetchMoviesList(
            query: requestValue.query,
            page: requestValue.page,
            cached: cached,
            completion: { result in

            if case .success = result {
                self.moviesQueriesRepository.saveRecentQuery(query: requestValue.query) { _ in }
            }

            completion(result)
        })
    }
}

먼가를 막 하는건 알겠는데 혹시 이상한 점을 찾으셨나요? 아니 Movie Entity 외에는 의존성을 띄면 안된다면서요! 그런데 저 moviesRpository는 뭔가요? 라고 생각하셨나요. 맞습니다. 다시 Clean Architecture 다이어그램을 확인해 보면

이렇게 작은 Flow Chart를 확인할 수 있는데 Use Case에서 Presenter를 호출하는 모습입니다. 어떻게? Use Case Output Port라는인터페이스를 활용해서 Presenter를 호출하는 모습입니다. Swift에서 인터페이스 == Protocol 이니 아하 우리는 Use Case에서 밖의 레이어를 호출할 때는 직접 호출하는게 아니라 Protocol을 타입으로 활용해 호출한다는 것을 알 수 있습니다.(의존성 역적의 법칙) 

 

MoviesRepository를 확인해보면 아래와 같은 인터페이스를 가지고 있습니다.

protocol MoviesRepository {
    @discardableResult
    func fetchMoviesList(
        query: MovieQuery,
        page: Int,
        cached: @escaping (MoviesPage) -> Void,
        completion: @escaping (Result<MoviesPage, Error>) -> Void
    ) -> Cancellable?
}

Interface Adapters

이 계층은 Presentation Layer라고도 불리며,  Entity 데이터를 그대로 표현 하는데 필요한 계층입니다. View, ViewModel, Coordinator, View의 이벤트에 관해 적용되는 UI 등이 여기 Layer층에 속합니다. 

 

Frameworks and Drivers

마지막 계층이며 DB, Network, 프레임워크 등이 포함되어 있는 레이어로 가장 낮은 수준의 모듈로 

DB -> Adapter -> Use Cases -> Entities 로 Dependency Rule을 가지고 있습니다.

 

Clean Architecture + MVVM

 

GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoor

Template iOS app using Clean Architecture and MVVM. Includes DIContainer, FlowCoordinator, DTO, Response Caching and one of the views in SwiftUI - GitHub - kudoleh/iOS-Clean-Architecture-MVVM: Tem...

github.com

앞서 설명드린 영화 검색 앱은 어떤 형태로 Clean Architecture + MVMM을 적용했는지 한번 살펴보겠습니다.

 

위의 이미지를 보시면 기존의 Clean Architecture 다이어그램에 빨간색의 동그라미 총 3개가 존재합니다. 레이어들을 그룹화 시켜서 나누어 두었는데  Domain, Presentation, Data 그룹으로 나누었습니다. 해당 프로젝트의 폴더링을 보시면 동일한 그룹으로 나누어져 있습니다.

 

Domain layer는 그래프의 가장 안쪽에 위치한 부분으로, 완전히 고립되어 있는 Layer이고 Entity, Use Case, Repository Interface가 포함되어있습니다. Repository Interface는 앞에서 언급한 인터페이스(의존성 역전을 위한 Protocol)를 말합니다.

 

Presentation layer는 View(UIViewController / SwiftUI View)와 ViewModel(Presenters)가 포함되며 View는 하나 혹은 여러개의 UseCase를 가지는 ViewModel에 의하여 조정되며 Presentation layer는 오직 Domain layer에 의존성을 두고있습니다.

 

Data layer는 앞서 Domain layer에서 가지고 있는 Repository Interface에 실재 코드인 Repository Implementation를 가지고 있고 하나 이상의 Data Source를 가지고 있습니다. 여기서 Data Source는 API를 통한 JSON 데이터 혹은 DB 입니다. 

 

Data Flow

데이터 흐름을 보자면

1. View에서 ViewModel에 있는 메서드를 호출합니다.

2. viewModel에서 Use Case를 실행하고

3. Use Case에서 User와 Repositories에서 데이터를 취합합니다. (의존성 역전)

4. 각 Repository는 Remote Data(Network), DB에서 데이터를 반환합니다.

5. 반환된 데이터가 View의 아이템 까지 전달되어 출력됩니다.

 

의존성의 방향을 보자면

Presentation Layer -> Domain Layer <- Data Repositories Layer

 

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

Domain Layer = Entities + Use Cases + Repositories Interfaces

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

 

 

출처: 

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

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

https://zeddios.tistory.com/1065