ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Clean Architecture 4(Data Layer)
    IOS/아키텍처 2024. 4. 5. 15:38

    Celan Architecture의 마지막 글입니다. Data Layer는 이전에 언급한 Domain Layer DIP에 따른 실제 구현체들이 있는 영역입니다.

     

    글을 읽기 전에 Domain Layer 파헤치기를 참고하셔서 같이 읽으시는 것을 추천합니다!  의존성 방향이 생성되는지 이해하는 게 목적인 소스 분석이기 때문에 가볍게 참고해 주세요!


    [이전 글]

    2024.03.10 - [IOS/아키텍처] - Clean Architecture 1(Layer 알아보기)

    2024.03.14 - [IOS/아키텍처] - Clean Architecture 2(Domain Layer 파헤치기)

    2024.04.04 - [IOS/아키텍처] - Clean Architecture 3(Presentation Layer)

     

    [참고]

    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

     

    [의존성 방향]

    시작하기 전에 다시 각 Layer층의 의존성 방향을 상기해 보겠습니다.

    Dependency Direction

    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

     

    1. Repository Intefrace Confromation

    final class DefaultMoviesRepository {
        
        private let dataTransferService: DataTransfer
        
        init(dataTransferService: DataTransfer) {
            self.dataTransferService = dataTransferService
        }
    }
    
    extension DefaultMoviesRepository: MoviesRepository {
        
        public func fetchMoviesList(query: MovieQuery, page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) -> Cancellable? {
            
            let endpoint = APIEndpoints.getMovies(with: MoviesRequestDTO(query: query.query,
                                                                         page: page))
            return dataTransferService.request(with: endpoint) { (response: Result<MoviesResponseDTO, Error>) in
                switch response {
                case .success(let moviesResponseDTO):
                    completion(.success(moviesResponseDTO.toDomain()))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
    }
    
    // MARK: - Data Transfer Object (DTO)
    // It is used as intermediate object to encode/decode JSON response into domain, inside DataTransferService
    struct MoviesRequestDTO: Encodable {
        let query: String
        let page: Int
    }
    
    struct MoviesResponseDTO: Decodable {
        private enum CodingKeys: String, CodingKey {
            case page
            case totalPages = "total_pages"
            case movies = "results"
        }
        let page: Int
        let totalPages: Int
        let movies: [MovieDTO]
    }
    ...
    // MARK: - Mappings to Domain
    
    extension MoviesResponseDTO {
        func toDomain() -> MoviesPage {
            return .init(page: page,
                         totalPages: totalPages,
                         movies: movies.map { $0.toDomain() })
        }
    }
    ...

     

    Domain영역에 있는 Repository를 구현하고 있는 소스입니다. 

    즉, DIContainer에선 API EndPoint, DB(CoreData NSPersistentContainer) 인스턴스를 생성해 Repository 구현체에 맵핑하고  UseCase에 다시 맵핑해 사용하는 흐름을 가지고 있습니다.

     

    간단히 말해 Domain영역에는 Protocol 즉 추상화만 참고하고 있기 때문에 실제 인스턴스는 DIContainer에 의해서 생성되고 동작하게 됩니다.

     

     

    요약 예제 코드

    더보기
    final class MoviesSceneDIContainer: MoviesSearchFlowCoordinatorDependencies {
        
        struct Dependencies {
            let apiDataTransferService: DataTransferService
            let imageDataTransferService: DataTransferService
        }
        
        private let dependencies: Dependencies
    
        // MARK: - Persistent Storage
        lazy var moviesQueriesStorage: MoviesQueriesStorage = CoreDataMoviesQueriesStorage(maxStorageLimit: 10)
        lazy var moviesResponseCache: MoviesResponseStorage = CoreDataMoviesResponseStorage()
        
        init(dependencies: Dependencies) {
            self.dependencies = dependencies        
        }
        
        // MARK: - Use Cases
        func makeSearchMoviesUseCase() -> SearchMoviesUseCase {
            DefaultSearchMoviesUseCase(
                moviesRepository: makeMoviesRepository(),
                moviesQueriesRepository: makeMoviesQueriesRepository()
            )
        }
        
        func makeFetchRecentMovieQueriesUseCase(
            requestValue: FetchRecentMovieQueriesUseCase.RequestValue,
            completion: @escaping (FetchRecentMovieQueriesUseCase.ResultValue) -> Void
        ) -> UseCase {
            FetchRecentMovieQueriesUseCase(
                requestValue: requestValue,
                completion: completion,
                moviesQueriesRepository: makeMoviesQueriesRepository()
            )
        }
        
        // MARK: - Repositories
        func makeMoviesRepository() -> MoviesRepository {
            DefaultMoviesRepository(
                dataTransferService: dependencies.apiDataTransferService,
                cache: moviesResponseCache
            )
        }
        func makeMoviesQueriesRepository() -> MoviesQueriesRepository {
            DefaultMoviesQueriesRepository(
                moviesQueriesPersistentStorage: moviesQueriesStorage
            )
        }
        func makePosterImagesRepository() -> PosterImagesRepository {
            DefaultPosterImagesRepository(
                dataTransferService: dependencies.imageDataTransferService
            )
        }
     }

    결론: 이렇게 직접 Repository 구현체를 DIContainer에 의해 UseCase에 맵핑되고, UseCase는 이 Repository의 추상화만 가지고 있기 때문에 DIP에 의한 의존성 역전으로 인해 DataLayer층이 Domain에 의존하게 되는, Clean Architecture의 의존성 방향에 맞게 설계된 것을 확인할 수 있습니다.

Designed by Tistory.