나는 이 Data Layer을 이렇게 세분화하여 구분하는 이유에 대해서 정말 많이 고민해 본 것 같다.
왜 VM에서 직접 서버에 Firebase접근을 통해 데이터를 가지고 오지 않고 다른 repository를 사용하고 왜 DataSource를 사용하는가?
이러한 부분에 대한 나름의 이유를 찾고 기록해야겠다고 생각했다.
우선 Data Layer에 대해서 알아보기 전에 내가 이해하기 위해 찾아본 내용부터 확인해보자.
1. API
2. Repository Pattern 과 Data Layer
3. 결론
순으로 정리함.
1. API
API는 비전공자인 나도 아주 많이 들어본 용어이지만,
아무리 읽고 이해를 하려고 해도 애매한 개념에 매번 헷갈린 용어이다.
일단 약자로는 Application Programming Interface로 서로 다른 애플리케이션끼리 데이터를 주고받는 일련의 규칙을 말한다.
아래의 설명 너무 좋음.
https://dev-dain.tistory.com/50
내가 이해한 API 는 앞서 설명한 추상화 개념처럼 고수준 언어를 사용하여 프로그램 간 통신이 가능하게 해주는 Interface이고
이는 정확한 설명서가 있어야 사용이 가능하다.
2. Repository Pattern 과 Data Layer
Repository 패턴이 Android Developer에서 설명하는 Data Layer의 핵심 키이다.
추상화가 뭔가?
앞전에 설명한 대로라면 추상화를 적절히 사용하면 각각 객체의 관심사를 분리하여되어 개발을 독립적으로 할 수 있다.
UI Layer에서 UI 상태를 관리하는 ViewModel은 이 Repository로부터 데이터를 가져오게 된다.
추상화를 통해 ViewModel은 자신이 Repository에서 받은 데이터가 어떤 경로로 만들어졌는가 알 필요가 없다.
심지어 이 데이터가 서버에서 와서 캐시로 저장된 후 보여지는 데이터인가 아니면 그냥 서버에서 바로 온 데이터인가의 비즈니스 로직의 부분도 몰라도 된다.(비즈니스 로직은 어플리케이션의 Data들의 생성 저장 조회 등을 하는 로직이다.)
이렇게 사용하는 게 Repository Pattern이다.
데이터의 운반 책임을 Repository에 넘기는 패턴.
그러면 이제 Repository의 역할을 추론할 수 있다.
1) 데이터를 제공해줌
2) 비즈니스 로직을 관리함
가장 큰 역할은 위 두가지 일 수 있다.
그럼 Repository는 어디서 데이터를 가지고 오는가?
아래의 그림에 답이 있다. Data Source다
UI Layer(추후의 Domain Layer)에서 소비하는 모든 데이터는 Repository를 통해서만 얻도록 설계하여야 한다.
이 말은 Repository만이 Data Source에 직접적으로 접촉하는 객체라는 말이다.
예제를 통한 설명을 하기 전에 Repository와 Data Source에도 적용되어야 하는 원칙이 있다.
각 Repository는 하나의 Data Type만을 담당해야 한다.
동아리 멤버 Data는 MemberRepository가 담당하고,
동아리 종류 Data는 PartyRepository가 담당하게 만든다.
지금 의문이 드는 건 그럼 두 Repository를 동시에 쓰는 경우는 어떻게 하냐?인데 이게 추후에 Domain Layer에서 다룬다.
Data Source는 하나의 Data Type과 하나의 Source에서만 데이터를 가져오는데 책임을 가지게 해야 한다.
Local인지 Network인지 File에서 가지고 오던지 하나의 Source에서 가져오게 해야 한다.
만약 Local과 Network , File 모두 하나의 Data Type의 데이터를 가지고 오고 싶다면, 3가지 다 만들어야 한다.
LocalMemberDataSource
NetworkMemberDataSource
FileMemberDataSource
...
자 그럼 예를 들면서 어떻게 사용하는가 알아보자
이번에도 동아리를 예로 들어야겠다.
현재 동아리 회원이 누가 있는가 확인하는 로직을 만들어 보자
임의의 Firebase API를 직관적으로 만들었고 이게 잘 동작한다는 가정을 해보자.(비동기 처리 등등 다 처리가 되었다고 가정)
class MemberListViewModel() : ViewModel(){
fun getAllMember() : List<String>{
val list = Firebase.getMember()
return list
}
}
아주 단순한 프로젝트의 경우 이 ViewModel을 사용하는데 문제점은 없을 수 있다.
다만 아무리 단순한 프로젝트라고 하더라도 이렇게 만들면 UI Layer의 테스트가 매우 힘들다.....
Data Layer를 만들지도 않았기 때문에 Data Layer를 테스트한다는 것도 말이 안 되는 구조다.
그럼 분리해보자
class MemberListViewModel(
private val memberRepository: MemberRepository
) : ViewModel(){
fun getAllMember() = memberRepository.getAllMember()
}
class MemberRepository(){
fun getAllMember() : List<String> = Firebase.getMember()
}
자 이제 UI와 Data Layer가 분리되었다.
여기서 어? Data Source 없이 저렇게 해도 되나?라고 생각이 들 텐데, 해도 되긴 함. 하지만 계속 말하듯이 테스트 편의성이니 뭐니 하면서 더 책임을 분리하고 ~~~~~ 해야 한다.
물론 여기서 Repository가 다른 데이터 소스도 사용하게 된다면 결국 Data Source를 나누는 과정이 필요하다.
그럼 DataSource까지 만들어 보자
class MemberRepository(
private val netWorkMemberDataSource: NetWorkMemberDataSource
){
fun getAllMember() : List<String> = netWorkMemberDataSource.getAllMember()
}
class NetWorkMemberDataSource(){
fun getAllMember() : List<String> = Firebase.getMember()
}
자 이제 됐나? 사실 더 책임을 분리하여 추상화시켜서 내려갈 수 있다.
class MemberRepository(
private val netWorkMemberDataSource: NetWorkMemberDataSource
){
fun getAllMember() : List<String> = netWorkMemberDataSource.getAllMember()
}
class NetWorkMemberDataSource(
private val memberDataApi: MemberDataApi
){
fun getAllMember() : List<String> = memberDataApi.getAllMember()
}
class MemberDataApi(
private val firebase : Firebase
){
fun getAllMember() : List<String> = firebase.getMember()
}
여기서 개발 도중에 아....
Repository를 MockRepository로 유닛 테스트하고 싶다 또는 새로 구현한 Repository로 이전하고 싶다... 하면 Interface로 Repository를 추상화시키고,
DataSource를 MockDataSource로 유닛 테스트하고 싶다 또는 새로 구현한 DataSource로 이전하고 싶다... 하면 Interface로 DataSource를 추상화시키고,
Api를 MockApi로 유닛 테스트하고 싶다 또는 새로 구현한 Api로 이전하고 싶다... 하면 Interface로 Api를 추상화시키고...
하면 된다
하나의 예를 보여준다면.
class NetWorkMemberDataSource(
private val memberDataApi: MemberDataApi
){
fun getAllMember() : List<String> = memberDataApi.getAllMember()
}
interface NetWorkMemberApi{
fun getAllMember(): List<String>
}
class MemberDataKtorApi(
private val ktor : Ktor
) : NetWorkMemberApi{
override fun getAllMember() : List<String> = ktor.getMember()
}
class MemberDataApi(
private val firebase : Firebase
):NetWorkMemberApi{
override fun getAllMember() : List<String> = firebase.getMember()
}
위와 같이 구성하여 DataSource에 종속성 주입할 때 관리를 하면 된다.
마지막으로 Repository를 통해 여러 DataSource에 접근하여 관리하는 예를 보자
Firebase에서 참여자 데이터를 가져오고 Firestorage에서 사진을 가져오고
사용자 정보를 LocalDataBase에 저장하는 것을 가정해보자
class MemberRepository(
private val netWorkMemberDataSource: NetWorkMemberDataSource,
private val localMemberDataSource: LocalMemberDataSource
){
fun getAllMember() : List<String> {
val list = netWorkMemberDataSource.getAllMember()
localMemberDataSource.saveAllMember(list)
return list
}
}
물론 아주아주 많은 것들이 생략된 함수이지만,
위처럼 하나의 Repository에서 비즈니스 로직을 관리하여 사용할 수 있다.
3. 결론
위의 내용은 대부분 왜 Repository Pattern을 사용하는가,
왜 Data Source를 사용하고 Interface를 사용하여 추상화하는가에 대해서만 언급했다.
실제로 비즈니스 로직을 구현할 때에는 Flow, Coroutine 등의 라이브러리를 사용하여 다양한 방법으로 처리를 하고,
Threading이나 에러 사항 등 많은 고려사항이 있으나
실 프로젝트의 기능에 따라서 자유롭고 다양하게 만들 수 있으므로
직접 경험을 많이 해보아야 한다고 생각한다.
Android Developer의 Data Layer설명에는 더 자세하고 많은 사항들을 설명하고 있고, Youtube에서도 이해가 가기 쉽게 동영상 설명을 제공하고 있으므로 나처럼 초보 개발자들은 꼭꼭 꼭 보고 많은 걸 얻었으면 좋겠다..
'안드로이드 읽어보기 > 5. Android Architecture' 카테고리의 다른 글
3) Data Layer 워밍업 / 안드로이드 아키텍쳐 (0) | 2022.04.19 |
---|---|
Android Architecture은 어떻게 해야할까? (2) UI Layer (0) | 2022.03.16 |
MVI에 대해서. (0) | 2022.03.16 |
MVVM이 무엇인가??? (0) | 2022.03.16 |
Android Architecture 어떻게 해야하는가?(1) (0) | 2022.03.15 |