안드로이드 맨땅에 Repository 박기
Repository. 사전적 의미부터 시작해보자.
repository 미국식 [rɪˈpɑːzətɔːri] 영국식 [rɪˈpɒzətri]
1. (어떤 것의 대량) 저장소
2. (지식·정보 등의) 보고(寶庫)
- 출처 : 네이버 사전
결국 레포지토리(혹은 리포지토리)는 무엇을 저장하기 위해 존재한다. 안드로이드에 아키텍처를 적용하면서 자주 보게되는 Repository. 이 레포지토리를 이해하려면 Repository 패턴도 같이 이해하는 것이 좋다.
Repository 패턴
타 블로그에서 Repository 패턴을 어떻게 정의했는지 가지고 와 보았다.
데이터 출처(로컬 DB인지 API응답인지 등)와 관계 없이 동일 인터페이스로 데이터에 접속할 수 있도록 만드는 것을 Repository 패턴이라고 한다. 레포지토리는 데이터 소스에 액세스하는 데 필요한 논리를 캡슐화하는 클래스 또는 구성 요소이다.
과거에 MVVM을 공부할 때도 레포지토리가 존재했다. 당시 참고했던 이미지를 보면 레포지토리를 잘 이해할 수 있다. Presentation 레이어(View, ViewModel)나 Domain 레이어에서 Data 레이어(Repository, Model 등)에 접근할 때, 데이터 소스의 위치(서버, Local DB)를 몰라도 일관성 있게 원하는 데이터를 취할 수 있도록 돕는 것이 Repository의 역할이다.
보통 레포지토리를 언급할 때, 캡슐화라는 용어가 등장한다. 이는 객체의 속성과 행위(함수)를 하나로 묶고 구현 내용의 일부 혹은 전체를 외부로부터 감추는 것을 얘기한다. 이 경우에는 데이터 레이어의 소스들이 캡슐화 대상이다.
Repository라는 인터페이스만 ViewModel이 접근한다. 그렇게 함으로서 레포지토리 너머의 데이터 소스가 추가 혹은 제거되는 변경이 있더라도 도메인 레이어나 프레젠트 레이어는 알 수가 없다. 캡슐화가 되었기 때문. 동시에 알 필요도 없다. 변경에 따른 작업은 레포지토리와 레포지토리 구현체에만 있기 때문이다. Repository가 없다면 Model 따로 Remote Data Source 따로 처리를 해줘야 했을 것이다. 이 두 가지(서버, 로컬) 데이터 소스를 동시에 사용하는 경우라면 더욱 작업량이 늘었을 것이다. 그래서 개발자는 Repository가 없을 때보다 있을 때 데이터 소스가 변경되는데 부담을 적게 느끼게 된다.
이렇게 레포지토리로 인한 중간 단계의 추상화로 모듈화가 명확해지고 유지보수성과 단위테스트 검증이 쉬워진다.
실습
기존 코드를 설명해보자면, API를 View에서 바로 호출하는 구조였다. View는 Presentation 레이어로, 여기서 사용될 데이터가 서버와의 통신을 통한 것인지 로컬 DB를 통한 것인지 알 필요가 없다. 참고로 이 경우에서는 로컬 DB를 사용하는 형태로 실습을 진행한다.
// 기존코드 : UserListActivity
ApiClient()
.getApiService().getUsers()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onNext = {
if (it?.data == null) {
Log.i(this.localClassName, "Data(List users) is null")
Toast.makeText(this, "더 이상 불러올 데이터가 없습니다.", Toast.LENGTH_SHORT).show()
} else {
adapter.setDataSet(it.data)
}
},
onError = {
it.printStackTrace()
},
onComplete = {
Log.i(this.localClassName, "* * * * Complete * * * *")
Toast.makeText(this, "정상적으로 데이터를 가지고 왔습니다.", Toast.LENGTH_SHORT).show()
}
)
여기서 모델이 되는 data(Data 레이어)와 View(Presentation 레이어)를 명확히 분리하고 이 사이를 연결해 줄 Repository를 추가할 것이다. 서버로부터 데이터를 실제로 가져오는 부분이다. 이를 위해 Interface로 된 Repository와 이를 구체화 할 RepositoryImpl을 만든다.
// 변경 코드
interface Repository {
fun getUsers(): Single<List<DataItem>>
fun updateUser(id: Int, updateItem: UpdateItem): Single<UpdateItem>
fun createUser(updateItem: UpdateItem): Single<UpdateItem>
}
class RepositoryImpl : Repository {
override fun getUsers(): Single<List<DataItem>> {
/**
* api의 옵저버블을 구독하고 데이터가 도착하면 어댑터에 데이터를 할당
* 데이터가 할당하기 전에 오류 코드를 확인하도록 설계해야 하지만 지금도 잘 동작은 함
*/
return ApiClient()
.getApiService().getUsers()
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.map { it.data }
}
// 중략
}
// 변경 코드 : UserListActivity
repository.getUsers()
.subscribeBy(
onSuccess = {
if (it.isEmpty()) {
Log.i(this.localClassName, "Data(List users) is null")
Toast.makeText(this, "더 이상 불러올 데이터가 없습니다.", Toast.LENGTH_SHORT).show()
} else {
adapter.setDataSet(it)
Log.i(this.localClassName, "* * * * Complete * * * *")
Toast.makeText(this, "정상적으로 데이터를 가지고 왔습니다.", Toast.LENGTH_SHORT).show()
}
},
onError = {
it.printStackTrace()
Toast.makeText(this, "데이터를 가지고 올 수 없습니다.", Toast.LENGTH_SHORT).show()
}
)
변경 전후 코드가 궁금하다면 아래 링크를 참고하길 바란다.
아쉬운 점
도메인 레이어에 해당하는 UseCase 구현이 없다. 레포지토리를 받아서 비즈니스 로직을 구현하는 부분이다. UseCase가 없어서인지 모델 구조도 좋지 않다. 클린 아키텍처의 가장 중심에 해당하는 도메인 레이어. 이 도메인 레이어에 해당하는 Entity과 서버로부터 데이터를 받아서 담는 Response의 분리를 좀 더 제대로 해야할 것 같다. 이 도메인 레이어 부분은 정확한 활용 방법을 몰라서 그런지 필요성을 못느끼고 있는데 추후 아키텍처를 구축하는 과정에서 필요시 공부해볼 예정이다.
자료 출처
- [Android] Using Koin in MVP-Repository Pattern for DI
- [Android] 배달앱 클론 스터디 1주차
- [Android, Architecture] 안드로이드 아키텍처 - Model편
- [Android, MVVM] MVVM 따라하기 - 2 (Model 구현)
- dlwls5201 / study-android-toy-project
- [안드로이드] Repository 패턴에 대한 고찰 (👍추천)
- [TECHCON 2019: MOBILE - Android]2.예제에서는 알려주지 않는 Model 이야기
- Repository 패턴에 대해서 정리해 보겠습니다