클린아키텍처-클론코딩 (1)

kudoleh/iOS-Clean-Architecture-MVVM를 보고 제 나름대로  공부해서 쓴 글이므로 틀린 부분이 있을수도 있습니다.

 

해당 프로젝트에는 클린 아키텍처 뿐만이 아니라 여러가지 패턴이 사용됩니다.

  • Coordinator 패턴
  • Repository 패턴
  • DIContainer
  • ViewModel Input, Output
  • Observable 패턴
  • Network Layer
  • DTO 패턴

여기서 Observable 패턴은 직접 코드로 구현이 되있었으나 Combine이나 RxSwift로 대체할 수 있으니 연습도 할겸 둘중 하나를 사용하기로 했습니다.

 

각 계층의 의존성을 도식화 한 모형을 보면 Domain 계층으로부터 Data 와 Present 계층이 의존하고 있습니다. 따라서 Data와 Presentation 계층보다 Domain 계층을 먼저 구현하는게 좋을것 같네요. 가장 안쪽에 있는 Entities부터 구현해봅시다.

 

필요한 Entities를 구현하도록 합니다. 해당 API의 reponse모델은 현재 프로젝트에서는 DTO로 구현이 되있습니다. 여기서 말하는 Entities는 직접 사용할 Model을 말합니다. 영화 목록에 관한 Model과 검색하기 위한 Query가 필요합니다. 이를 구현합니다.

import Foundation

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

 

여기서 Equatable과 Identifier 프로토콜을 채택을 했는데, Equatable 프로토콜은 해당 객체를 ==, != 과 같은 연산자를 이용하여 비교할 수 있게 해줍니다. Identifier는 고유한 개체인지 확인하기 위한 프로토콜입니다. 아무래도 SwiftUI에서 list를 만들때 사용하려고 해당 프로토콜을 사용한 것 같습니다.

영화를 검색하는 쿼리문 역시 Entities에 포함됩니다.

struct MovieQuery: Equatable {
    let query: String
}

그 다음으로는 추상화된 Repository를 작성합니다. UseCase에서 의존성 역전법칙에 의해 해당 추상화된 Repository를 의존하고 있기 때문이죠.

추상화된 Repository를 작성하기 전에 Cancellable을 작성합니다. Cancellable는 작업을 취소 할 수 있게 해주는 프로토콜입니다.

protocol Cancellable {
    func cancel()
}

해당 프로토콜을 구현했으면 이제 추상화된 Repository를 작성합니다.

protocol MoviesRepository {
    @discardableResult
    func fetchMoviesList(query: MovieQuery, page: Int, cached: @escaping (MoviesPage)->Void, completion: @escaping (Result<MoviesPage, Error>)-> Void) -> Cancellable?
}
protocol MoviesQueriesRepository {
    func fetchRecentQueries(maxCount: Int, completion: @escaping (Result<[MovieQuery], Error>) -> Void)
    func saveRecentQuery(query: MovieQuery, completion: @escaping (Result<MovieQuery, Error>) -> Void)
}
protocol PosterImagesRepository {
    func fetchImage(with imagePath: String, width: Int, completion: @escaping (Result<Data, Error>) -> Void) -> Cancellable?
}

이렇게 데이터를 가져오는 작업을 추상화 시키고 추상화된것에 의존하게 하는것이 Repository패턴이라 합니다.

MoviesRepository 위에 있는 @discardableResult는 해당 함수의 결과값이 사용되지 않을때 경고나 뜨지 않게 해주는 것 입니다.

 

해당 프로젝트에서는 UseCase를 2가지 방법으로 구현하는것을 제시하고 있습니다. 

첫번째는 프로토콜로 구현할 UseCase를 미리 추상화하고, 해당 프로토콜을 채택하여 구현하는 방법입니다. SearchMovieUseCase가 이런식으로 구현 되어있습니다.

final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {
    
    private let moviesRepository: MoviesRepository
    private let moviesQueriesRepository: MoviesQueriesRepository
    
    init(moviesRepository: MoviesRepository, moviesQueriesRepository: MoviesQueriesRepository) {
        self.moviesRepository = moviesRepository
        self.moviesQueriesRepository = moviesQueriesRepository
    }
    
    func excute(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
}

두번째 방법은 같은 UseCase 프로토콜을 채택하는 방법입니다. UseCaser가 공통적으로 채택할 프로토콜을 만들고, 채택하고, start에 구현을 합니다.

public protocol UseCase {
    @discardableResult
    func start() -> Cancellable?
}
final class FetchRecentMovieQuriesUseCase: UseCase {
    
    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.fetchRecentQueries(maxCount: requestValue.maxCount, completion: completion)
        return nil
    }
    
    
}

struct RequestValue {
    let maxCount: Int
}

첫번째는 방법은 excute라는 함수가 추상화된 Repository에 의해 영화리스트를 fetch 합니다. 또 movie와 movieQueries Repository를 외부에서 주입받습니다. 

두번째 방법은 UseCase 프로토콜을 채택, Coordinator 패턴처럼 start에 데이터를 가져오는 코드를 작성합니다. 첫번째 방법의 excute 함수가 start가 된것 같습니다. requestValue, completion 블록, Repository를 외부로부터 주입받습니다.  다만 여기서 start가 nil이 리턴 되도록 되있는데, 클린코드에서 nil값을 반환하는 코드를 작성하지 말라고 되있는데.. 이건 나중에 한번 알아봐야 할 것 같습니다. 

이렇게 Domain 계층이 구현이 완료되었습니다.