ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Clean Architecture 2(Domain Layer 파헤치기)
    IOS/아키텍처 2024. 3. 14. 11:10

    지난 글에 이어서 작성한 글입니다. Clean Architecture의 구성 요소의 정의와 Data Flow가 궁금하시다면 아래 링크를 참고해주세요.

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

     

    Clean Architecture 1(Layer 알아보기)

    어떻게 하면 관리사 분리부터 유지보수에 용이한 구조를 만들 수 있을까 고민이 들어 Clean Architecture에 대해 공부한 걸 정리하려 합니다. Clean Architecture를 보면 기본 컨샙은 다음과 같습니다. Depen

    jjunbbang.tistory.com

    이번엔 MVVM structual design pattern을 활용한 소스코드를 참고하며 분석해 보는 시간을 가져보겠습니다. 해당 앱은 저작자가 영화검색 서비스 앱을 예시로 실제 소스들을 보면서 따라가면 좋을 거 같아 블로깅합니다.

     

    이번 포스팅은 Domian Layer에 대해서 알아보겠습니다.


    Domain Layer

    Clean Architecture의 각 Layer에 해당하는 폴더들이 있습니다. Domain, Presentation, Data 

    Domain은 비즈니스 규칙에 해당하는 영역으로 Presentation, Data Layer 영역이 의존하는 상위 모듈입니다. 

    Domain 같은 경우엔 Interface(DIP 의존성 역전)을 활용해 하위모듈 Data Layer의 의존성을 없앴습니다. Presentation 영역의 경우 UIKit 프레임워크를 사용해 domain 영역과 상호작용(request, response)을 하고 있습니다.

     

     

     

     

     

    핵심 코드는 다음과 같습니다.


    Entities

    비즈니스 모델에 의해 생성되는 데이터 즉 클래스와 메소드가 가능한 객체들입니다.

    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]
    }

     


    UseCase

    protocol UseCase {
        @discardableResult
        func start() -> Cancellable?
    }
    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
        }
    }

    위 소스코드에서 UseCase protocol의 경우 useCase의 동작을 추상화해 실제 UseCase를 좀 더 Generic 하게 사용한 방법입니다.

    다른 예시는 다음과 같습니다.

    protocol SearchMoviesUseCase {
        func execute(
            requestValue: SearchMoviesUseCaseRequestValue,
            cached: @escaping (MoviesPage) -> Void,
            completion: @escaping (Result<MoviesPage, Error>) -> Void
        ) -> Cancellable?
    }
    
    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)
            })
        }
    }
    
    struct SearchMoviesUseCaseRequestValue {
        let query: MovieQuery
        let page: Int
    }

    protocol을 사용해 usecase가 동작해야 할 기능을 추상화하고 실제 usecase에선 protocol을 conform 하여 실제 동작을 implement 하고 있는 모습입니다.

     

    이렇게 함으로써 테스트와 유지보수(Usecase 확장 시 일관되게 변경)에 용이한 구조로 사용하려는 의도가 보입니다.


    Interface

    Domain 영역에서 Data영역의 의존성을 제거하기 위해 사용된 추상화입니다.

    이렇게 함으로써 Repository type이 변경되더라도 Domain영역에 영향을 끼치지 않게 됩니다.

    //Interface Folder
    protocol MoviesQueriesRepository {
        func fetchRecentsQueries(
            maxCount: Int,
            completion: @escaping (Result<[MovieQuery], Error>) -> Void
        )
        func saveRecentQuery(
            query: MovieQuery,
            completion: @escaping (Result<MovieQuery, Error>) -> Void
        )
    }
    
    // UseCase Folder 
    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
        }
        
        final class DefaultMoviesQueriesRepository {
        
        private var moviesQueriesPersistentStorage: MoviesQueriesStorage
        
        init(moviesQueriesPersistentStorage: MoviesQueriesStorage) {
            self.moviesQueriesPersistentStorage = moviesQueriesPersistentStorage
        }
    }
    
    // Data Folder
    final class DefaultMoviesQueriesRepository {
        
        private var moviesQueriesPersistentStorage: MoviesQueriesStorage
        
        init(moviesQueriesPersistentStorage: MoviesQueriesStorage) {
            self.moviesQueriesPersistentStorage = moviesQueriesPersistentStorage
        }
    }
    
    extension DefaultMoviesQueriesRepository: MoviesQueriesRepository {
        
        func fetchRecentsQueries(
            maxCount: Int,
            completion: @escaping (Result<[MovieQuery], Error>) -> Void
        ) {
            return moviesQueriesPersistentStorage.fetchRecentsQueries(
                maxCount: maxCount,
                completion: completion
            )
        }
        
        func saveRecentQuery(
            query: MovieQuery,
            completion: @escaping (Result<MovieQuery, Error>) -> Void
        ) {
            moviesQueriesPersistentStorage.saveRecentQuery(
                query: query,
                completion: completion
            )
        }
    }

     

     

    'IOS > 아키텍처' 카테고리의 다른 글

    Clean Architecture 4(Data Layer)  (0) 2024.04.05
    Clean Architecture 3(Presentation Layer)  (0) 2024.04.04
    Clean Architecture 1(Layer 알아보기)  (0) 2024.03.10
Designed by Tistory.