서론
지금까지 ViewController의 역할을 분리하는 방법은 많이 알아보았습니다.
MVVM의 ViewModel은 ViewController의 비즈니스 로직을 분리하기 위함이고,
이전 포스팅에서 알아본 Coordinator 패턴도 ViewController의 Flow 로직을 분리합니다.
그렇다면 지금의 ViewModel의 역할은 가벼울까요? (무거우니 포스팅을 썼겠죠? ㅎ;)
이번 포스팅에서 이를 주제로 알아보고,
ViewModel의 역할을 덜어주는 Repository 패턴에 대해 알아봅시다.
Repository 패턴
Repository는 데이터 fetch 역할을 담당하는 객체입니다.
기존에는 ViewModel이 네트워크 통신도 담당을 했는데요.
네트워크 통신은 굳이 ViewModel이 몰라도 되는 정보입니다.
예를 들어, ViewModel이 서버에서 데이터를 가져오려고 하는데
서버가 개발 서버와 릴리즈 서버로 나뉘어 있다고 합시다.
ViewModel의 입장에서 서버가 개발 서버인지, 릴리즈 서버인지 중요할까요?
아닙니다. 어디에서 가져오든 데이터만 제대로 가져오면 되는거에요.
심지어 서버가 아니라 캐시에서 가져와도 상관 없죠.
이 역할을 도와주는 것이 Repository 입니다.
Repository를 도입해서 데이터 로직과 비즈니스 로직을 분리하고,
객체 간의 결합도가 감소하여 테스트, 유지보수, 협업에 유리합니다.
추가로, iOS Clean Architecture에서는 UseCase라는 레이어를 하나 더 넣어서
ViewModel -> UseCase -> Repository 순서로 의존을 형성했는데요.
ViewModel이 UseCase를 가지고 있고, 특정 UseCase를 실행하면
UseCase가 Repository에 접근해서 데이터 로직을 수행하게 합니다.
iOS Clean Architecture와 UseCase는 조금 더 나중에 다뤄보겠습니다.
Repository 예제
이번 포스팅은 직접 만든 예제가 아니라,
유명한 Clean Architecture + MVVM에서 가져왔습니다.
먼저 Repository 객체입니다.
final class DefaultMoviesQueriesRepository {
private let dataTransferService: DataTransferService
private var moviesQueriesPersistentStorage: MoviesQueriesStorage
init(dataTransferService: DataTransferService,
moviesQueriesPersistentStorage: MoviesQueriesStorage) {
self.dataTransferService = dataTransferService
self.moviesQueriesPersistentStorage = moviesQueriesPersistentStorage
}
}
Repository는 저장소에 관련된 프로토콜을 가지고 있습니다.
상세 타입이 아니라 프로토콜 타입으로 선언되어서 이를 준수하는 모든 객체를 사용할 수 있습니다. (재활용성 증가)
protocol DataTransferService {
typealias CompletionHandler<T> = (Result<T, DataTransferError>) -> Void
@discardableResult
func request<T: Decodable, E: ResponseRequestable>(with endpoint: E,
completion: @escaping CompletionHandler<T>) -> NetworkCancellable? where E.Response == T
@discardableResult
func request<E: ResponseRequestable>(with endpoint: E,
completion: @escaping CompletionHandler<Void>) -> NetworkCancellable? where E.Response == Void
}
protocol MoviesQueriesStorage {
func fetchRecentsQueries(maxCount: Int, completion: @escaping (Result<[MovieQuery], Error>) -> Void)
func saveRecentQuery(query: MovieQuery, completion: @escaping (Result<MovieQuery, Error>) -> Void)
}
위의 Repository가 가지고 있는 프로토콜입니다.
DataTransferService는 서버에서 데이터를 가져오는 프로토콜이고,
MoviewQueriesStorage는 코어 데이터에서 데이터를 가져오는 프로토콜입니다. (코어 데이터는 로컬 DB 역할입니다.)
마지막으로 ViewModel은 UseCase를 통해 Repository를 사용합니다.
//ViewModel 코드
private func load(movieQuery: MovieQuery, loading: MoviesListViewModelLoading) {
...
moviesLoadTask = searchMoviesUseCase.execute(
...
})
}
//UseCase 코드
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
...
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)
})
}
}
이제 ViewModel은 데이터를 가져오는 곳에 상관 없이 자신의 비즈니스 로직을 수행할 수 있게 되었습니다.
마무리
Repository 패턴은 이해가 가장 쉬운데 효과는 탁월한 디자인 패턴이라고 생각합니다.
왜 사용하는지, 어떻게 사용되는지가 명확하기 때문에 이해가 쉽다고 생각합니다.
또, 적용을 했을 때 개선된 점이 바로 다가오기 때문에 프로젝트 개선 효과도 탁월한 것 같아요.
MVVM 패턴에서 ViewModel의 역할이 많다고 생각이 든다면
Repository 패턴을 사용해보시는 것을 추천합니다.
감사합니다!
참고
https://4z7l.github.io/2020/11/24/repository-pattern.html
https://eunjin3786.tistory.com/198
https://namget.tistory.com/entry/안드로이드-Clean-Architecture