-
Clean Architecture 2(Domain Layer 파헤치기)IOS/아키텍처 2024. 3. 14. 11:10
지난 글에 이어서 작성한 글입니다. Clean Architecture의 구성 요소의 정의와 Data Flow가 궁금하시다면 아래 링크를 참고해주세요.
2024.03.10 - [IOS/아키텍처] - Clean Architecture 1(Layer 알아보기)
이번엔 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