안드로이드 클린 아키텍쳐에 대하여 객관적으로 알려드리기 위해, [안드로이드 공식 홈페이지의 권장 아키텍쳐]를 토대로 말씀드리고 있음을 밝히며 글을 시작해볼까 해요.
사실 안드로이드 개발을 하는데 있어 가장 좋은 방법은 뭘까요? 그것은 두 가지로 말씀드릴 수 있을것 같아요. 하나는 [안드로이드 공식 홈페이지]를 통해 지식을 익히고 앱을 개발해 나아가는 것이고 두 번째는 안드로이드 GDE(Google Developer Expert)분들이 쓴 블로그 칼럼(ex. [Manuel Vivo님의 블로그])을 참고하는 것이라 생각해요.
[앱을 개발하는 좋은 방법?]
- 안드로이드 공식 홈페이지
- 안드로이드 GDE분들의 블로그 칼럼
그래서 어쩌면 이 글을 읽으시는 분들도 이 블로그 글을 읽는것 보단 안드로이드 공식 홈페이지를 읽는게 더 나을 수도 있다고 생각합니다. 하지만, 한가지 확실히 말씀드릴 수 있는 것은 안드로이드 공식 홈페이지와 공식 블로그 칼럼을 응용한 펜타시큐리티 모바일팀의 작은 지식을 얻어가실 수 있다는 것이예요.
그럼 이제 본격적으로 시작해볼까 합니다.
일전에 첨부드린, [안드로이드 공식 홈페이지 아키텍쳐 권장 가이드]에선, '관심사의 분리'를 꼽고 있습니다. 그리고 안드로이드 앱 아키텍쳐에는 3가지 레이어가 존재하며 이들은 각각의 '책임'을 명확히 가지고 있습니다.
안드로이드 클린 아키텍쳐를 이해하기 위한 첫 걸음은 다음으로 요약할 수 있습니다.
[안드로이드 클린 아키텍쳐 이해의 첫 걸음]
1. 안드로이드 각 레이어 모듈이 무엇이 있는지 아는 것.
2. 각 레이어들 모듈들의 책임이 무엇인지 아는 것.
이러한 3가지 레이어들, UI Layer, Domain Layer, Data Layer가 바로 그것이죠. 이 3가지 레이어들을 설명하기에 앞서, 해당 레이어들이 어떻게 통신하며 동작하는지 알면 이해에 더 도움이 될것 겉아요!
추후 나올 다이어그램의 화살표 방향과 헷갈릴 수 있을것 같아 말씀드려요.
위의 화살표는 모듈들간의 의존관계를 뜻합니다. 즉, 위 Layer모듈들은 단방향의 참조를 의미합니다.
출처 : [안드로이드 공식 홈페이지]
[출출하여.. 배달의 민족을 시키는 우리...]
우린 너무 배가 고파... 배달의 민족 앱에 들어갔습니다. 그리고 위 메뉴 중, '분식'이 오늘따라 땡기네요? 그래서 분식을 클릭했습니다.
하지만!
이런 단순한 동작 하나라를 실행하더라도 앱 내에서는 아까 말씀드린 3가지 레이어들의 통신이 시작됩니다. 그리고 그러한 레이어들은 단방향 데이터 흐름으로 통신하게 되죠. (UpStream + DownStream)
여기서 '단방향 데이터 흐름'은 안드로이드 클린 아키텍쳐에서 너무 중요한 사항이라 정리하고 갈게요!
단방향 데이터 흐름이란?
이름에서도 알 수 있다시피, 데이터가 오로지 한 방향으로만 흐르는 것을 의미합니다. 그리고 이러한 흐름은 두 가지가 있습니다. 하나는 'Up Stream방식', 또 다른 하나는 'Down Stream'입니다.
- Up Stream : 사용자가 클릭 등의 이벤트를 발생시킴으로써 Ui -> Domain -> Data로 전달함으로써 상위 레이어로 전달하는 방식
- Down Stream : Remote or Local Server로부터 받은 데이터를 Data -> Domain -> Ui로 전달함으로써 하위 레이어로 전달하는 방식
참고 : [안드로이드 공식 홈페이지 단방향 데이터 흐름]
좀 더 자세히 말씀드려볼까요? 우리가 '분식'메뉴를 클릭했다고 가정해 보겠습니다. 그럼 아래의 그림과 같이 이벤트가 전송됩니다.
[사용자 이벤트에 따른 데이터 UpStream의 흐름 예시]
어때요? 생각보다 간단하죠?
분식 메뉴를 클린한 우리는 배달의 민족 앱이 재빠른 분식 리스트를 Response하길 원합니다... 그리고 배달의 민족 앱이 이에 응답해 줬습니다. 우리에게 일용할 양식 리스트를 내려주었어요.
[분식 클릭 후, 양식 리스트 Response]
오늘은 집가서 무조건 떡볶이다...
그리고 위처럼 리스트가 보이는 과정 자체. 그 자체가 데이터 DownStream을 의미합니다!
[사용자 이벤트에 응답 데이터 DownStream의 흐름 예시]
즉, 하나의 이벤트가 발생했을 시, 상위 레이어에서 하위 레이어로 내려오는 경우는 없어요. 반대도 마찬가지죠!
지금까지 안드로이드 아키텍쳐 레이어엔 3가지(UI + Domain + Data)레이어가 있고, 이들은 단방향 데이터 흐름(UpStream + DownStream)으로 통신한다는 것을 아주 쉽게 이해했을거라 생각합니다! 이제 좀 더 세부적으로, 각각의 레이어들에 대해 말씀드려볼게요~!
[참고]
안드로이드 클린 아키텍쳐를 구현하는데 있어, 단방향 데이터 흐름을 구현하기 위해 Reactive Stream라이브러리인 'Flow'를 활용합니다. 이에 대한 글은 [ReactiveStream Flow 활용하기]를 참고하시면 좋을거라 생각해요!
UI레이어에 해당하는 모듈은 다음과 같습니다.
- [UI Elements Module]
- Activity/Fragment + xml view
- ComposeUI
- [UI State Holder Module]
- ViewModel
출처 : [안드로이드 공식 홈페이지 UI Layer]
이 글을 읽으시는 분들은 UI Elements Module들에 대해선 너~무 잘 아실거라 생각해요. 이 모듈들은 말 그대로 UI 요소 자체(ex. Activity, Fragment, Dialog, ConstraintLayout, TextView...)를 의미합니다.
그리고 요즘에 [Hot한, 선언형 UI방식인 Compose]도 이 부분에 속합니다. Compose또한 UI요소 그 자체를 의미하니깐요.
하지만, 아키텍쳐 관점으로 봤을 때, UI Layer에서 정말 중요한 부분은 ViewModel입니다. 이 모듈은 UI에 바인딩되는 데이터를 보유하고 있는 모듈이죠. 이 모듈을 위 배달의 민족 '분식'리스트 예시를 통해 표현해 볼게요.
{
shops:[
{
shopName:"모자이크된 이름",
shopThumbnail:"https://~~"
score:"4.9",
reviewCount:"121",
couponType:"normal"
merits:"[맛집검증+신속배달]+왕벌떡볶이세트",
minimumOrderPrice:14000,
deleveryType:"fix",
deleveryTip:2500,
deleveryMinimumMinute:40,
deleveryMaximumMinute:55,
},
{
shopName:"모자이크된 이름",
shopThumbnail:"https://~~"
score:"4.9",
reviewCount:"221",
couponType:"normal"
merits:"꿀떡 시그니처, 가밀떡볶이",
minimumOrderPrice:8000,
deleveryType:"range",
deleveryMinimumTip:1000,
deleveryMaximumTip:4000,
deleveryMinimumMinute:39,
deleveryMaximumMinute:54,
},
{
shopName:"모자이크된 이름",
shopThumbnail:"https://~~"
score:"4.9",
reviewCount:"121",
couponType:"fast"
merits:"옆집떡볶이, 2인떡볶이+치즈+비엔나+...",
minimumOrderPrice:9000,
deleveryType:"range",
deleveryMinimumTip:0,
deleveryMaximumTip:1500,
deleveryMinimumMinute:33,
deleveryMaximumMinute:48,
},
...
]
}
위는 배달의 민족 메인화면의 데이터를 '추측'해본 JSON구조예요. 안드로이드 앱 내에서 위와 같은 데이터 구조들이 UI에 바인딩 됨으로써 분식 리스트 화면을 보여주게 됩니다.
그리고 위의 데이터를 아래와 같은 형식으로 보유하는 경우가 많습니다. android에서 권장하는 상태 관리 홀더 클래스인 'LiveData'와 'StateFlow'의 예시로 들어보면 다음과 같을 수 있지요.
[LiveData버전]
class ShopListViewModel: viewModel{
private val _shopItem = MutableLiveData<List<ShopItem>>()
val shopItem : LiveData<List<ShopItem>>
get() = _shopItem
fun ...() {}
}
[StateFlow버전]
class ShopListViewModel: viewModel{
private val _shopItem = MutableStateFlow<List<ShopItem>>(emptyList())
val shopItem = _shopItem.asStateFlow()
fun ...() {}
}
참조 : [안드로이드 Flow활용 권장 사례]
LiveData를 사용하든, StateFlow를 사용하든, ViewModel에서는 상태 홀더 클래스를 보유하며, 이를 통해 UI에 데이터를 바인딩하는 방식. 이것이 바로 ViewModel의 책임입니다.
너무 중요해 한번 더 강조하고 가겠습니다.
[ViewModel의 책임]
- UI에 바인딩해준 데이터를 상태 홀더 클래스(LiveData or StateFlow)를 통해 보유한다.
그리고 책임이 두 가지 더 있습니다. 이들을 UpStream과 DownStream관점으로 말씀드려볼게요.
[UpStream관점]
ViewModel은 '비즈니스 로직의 출발 지점'입니다. 이전에 보여드렸던, 사용자의 '분식메뉴 클릭'을 기억하시나요? 이런 행위를 하기 위해 ViewModel은 사용자로부터 UI이벤트를 수신받아야 합니다. 그리고 이는 DomainLayer나 DataLayer에 이벤트를 전달하며 비즈니스 로직의 시작을 트리거하게 됩니다.
[DownStream관점]
ViewModel은 DomainLayer or DataLayer로부터 UI에 바인딩될 데이터 구조를 응답받습니다. 그리고 이를 통해 UI에 데이터바인딩을 진행하게 돼죠.
즉, ViewModel의 책임은 사용자의 이벤트를 받아 비즈니스 로직을 시작시켜준다는 점(=UpStream)이자 DomainLayer or DataLayer로부터 받은 데이터를 UI에 바인딩시켜주는것(=DownStream)입니다. 그리고 그렇게 받은 데이터를 상태 홀더 클래스(=StateFlow or LiveData)를 통해 보유한다는 것이죠! 한번 더 반복합시다!
[ViewModel의 책임]
- UpStream관점에서 사용자의 이벤트를 받아 비즈니스 로직을 시작시키는 책임
- DownStream관점에서 상위 레이어로부터 받은 데이터를 UI에 바인딩해주는 책임
- UI에 바인딩해준 데이터를 상태 홀더 클래스(LiveData or StateFlow)를 통해 보유한다.
이제 개념에 대해 알아봤으니, 실제 소스코드를 통해 알아봐야겠죠? 아래는 2가지 class가 있어요.
[클래스 구조]
- UI Elements의 책임을 가지고 있는 'MainActivity'
- UI State Holder의 책임을 가지고 있는 'MainViewModel'
@AndroidEntryPoint
class MainActivity: AppCompatActivity{
priate val viewModel: MainViewModel by injects()
priate val searchAdapter by lazy { SearchAdapter() }
fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding {
btnSearch.setOnClickListener {
viewModel.search($"etSearch.text")
}
rvMain.apply {
setHasFixedSize(true)
adapter = mainPagingAdapter
}
}
lifecycleScope.launch {
repeatOnLfecycle(Lifecycle.State.RESUMED) {
launch {
viewModel.searchResults.collectLatest { searchResults ->
adapter.submitData(searchResults)
}
}
}
}
}
}
@HiltViewModel
class MainViewModel @Inject constructor(
private val searchRepository: SearchRepository
) {
private val searchResults = MutableStateFlow<List<SearchUiState>>(emptyList())
val _searchResults = searchResult.asStateFlow()
fun search(keyWord: String) {
viewModelScope.launch {
searchRepository.fetchSearchResults(keyWord)
.flowOn(Dispatchers.IO)
.map { Result.success(it) }
.catch { emit(Result.failure(it)) }
.collect { result ->
result.fold(
onSuccess = { searchUiStates ->
_searchResults.update { searchUiStates }
},
onFailure = { }
)
}
}
}
}
아마 아키텍쳐가 생소하신 분들은 위 코드가 약간 어려울 수도 있을것 같아요. 그래서 제가 족집게로 딱! 설명을 해드릴게요!
[UI Elements]
- UpStream관점 : onClick을 함으로써 viewModel의 비즈니스 로직을 호출하고 있어요. (search메소드)
- DownStream관점 : viewModel로부터 받은 데이터를 리사이클러뷰의 어댑터로 전송해주고 있죠. (viewModel.searchResults.collectLatest)
[UI State Holder]
- UpStream관점 : searchResultRepository에 검색 결과를 요청하고 있어요.
- DownStream관점 : searchResultRepository로부터 받은 데이터를 상태 홀더 클래스인 StateFlow에 저장하고 있고, 이를 통해 ui에 데이터를 바인딩하고 있어요.
좀 더 자세히 설명을 드리고 싶지만, 이 이상은 포스팅이 너무 길어질것 같아요. 그래서 다음 포스팅으로 미루도록 할게요. 더욱 자세한 이해를 원하신다면! [UILayer의 책임은 무엇인가]를 참고해 보시길 바랄게요!
안드로이드 공식 홈페이지의 Domain Layer페이지를 보시면 아시겠지만, DomainLayer는 '필수'라기보단 '선택'입니다. (Optional)이라고 표시된 부분 보이시나요?
결론부터 말씀드릴게요. DomainLayer는 '안넣어도 상관은 없습니다.' 하지만 앱이 커지고 서비스가 커진다면? 그때부터는 DomainLayer의 필요성을 절감할 수 있다는 말씀을 드리고 싶어요. 그리고 이 글을 쓰는 저도 개인적으로 '필수'라고까지 생각하고 있는데요. 그럼 그렇게 대단한 DomainLayer가 대체 무엇일까요? 그리고 어떻게 사용하는 것일까요?
Domain레이어에 해당하는 모듈은 다음과 같습니다.
하지만, 이 UseCase모듈은 기존 모듈과는 신기한 방식으로 설계됩니다. 소스코드를 먼저 보여드리며 설명하는게 더 빠를것 같네요!
class FormatDateUseCase @Inject constructor(
private val userRepository: UserRepository
) {
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
우선, UseCase모듈의 네이밍이 조금 특이하지 않나요? UseCase모듈은 class인데 네이밍이 동사로 시작하죠. 하지만 이는 [Android DomainLayer에서 권장하고 있는 네이밍 컨벤]에 따라 정해진 부분이예요.
[UseCase모듈 네이밍 규칙]
- 현재 시제의 동사 + 명사/대상(선택사항) + UseCase
그리고 이 UseCase모듈의 사용 방식을 알면 네이밍 지정 방식이 납득갈거라 생각합니다.
invoke함수에 대해 알고 계신가요? UseCase모듈 이해를 위해 간단히 짚고 넘어가보겠습니다. 이는 kotlin언어단에서 제공해주는 'operator관례 함수'중 하나입니다. 그리고 이러한 operator함수의 네이밍을 어떻게 지정하느냐에 따라 kotlin에서의 코드 가독성을 향상시킬 수도 있죠. 자세한 사항은 [kotlin 공식 홈페이지의 operator관례]를 참고해보시면 도움이 될거라 생각합니다.
그럼 이제 invoke함수의 operator관례에 대해 알아보겠습니다. 위에 예제로 제시한 operator fun invoke()함수가 보이시나요? 위 함수는 호출부에서 아래와 같은 방식으로 사용됩니다.
class HomeViewModel @Inject constructor(
private val formatDataUseCase: FormatDataUseCase
): ViewModel() {
fun getUserInfo() {
val date = formatDataUseCase()
}
}
혹시 기억하시나요...? FormatDataUseCase클래스 내에는 분명 'formatDataUseCase()'라는 함수를 따로 정의해주지 않았습니다. 단순, 'operator fun invoke()'함수만 정의되어 있을 뿐이죠. 이를 통해 추측이 되지 않나요...?
operator fun invoke함수는 클래스의 이름을 그대로 사용하면서 invoke 메소드를 호출할 수 있게 해주는 kotlin관례입니다. 따라서 아래는 위와 같은 코드예요
class HomeViewModel @Inject constructor(
private val formatDataUseCase: FormatDataUseCase
): ViewModel() {
fun getUserInfo() {
val date = formatDataUseCase.invoke()
}
}
이제 DomainLayer의 가장 중요한 operator invoke함수에 대해 배웠습니다. 그럼 다음 의문이 드실 수 있을꺼예요.
그럼... DomainLayer의 UseCase모듈을 왜 사용하는 것이지...? 그리고 어떻게 사용하는 것이지...?
이에 대한 답을 찾기 위해선, 일전에 설명드린 'ViewModel'의 책임에 대해 기억할 수 있어야 합니다. 일전에 ViewModel의 책임은 3가지가 된다고 말씀드렸는데요.
[ViewModel의 책임]
- UpStream관점에서 사용자의 이벤트를 받아 비즈니스 로직을 시작시키는 책임
- DownStream관점에서 상위 레이어로부터 받은 데이터를 UI에 바인딩해주는 책임
- UI에 바인딩해준 데이터를 상태 홀더 클래스(LiveData or StateFlow)를 통해 보유한다.
따라서 위 책임에 관련되지 않은 부분은 UseCase패턴으로 분리한다면...? 더욱 깔끔한 코드가 될 수 있습니다! 그럼 어떤 경우가 있을까요? 아래와 같은 경우가 있을 수 있어요.
[UseCase패턴의 책임?]
- 상기 ViewModel책임 이외의 비즈니스 로직의 책임
- 여러 ViewModel에서 사용되는 중복코드를 정의하는 책임
참고 : [안드로이드 공식 홈페이지 Domain Layer]
그럼 이제 실제 소스코드를 봄으로써 이 UseCase가 어떤 방식으로 쓰이는지를 본다면 더 좋겠죠? 이 포스팅은 특히 길어질것 같아, [DomainLayer의 책임은 무엇인가?]에 포스팅해 놓았습니다.
이 레이어의 모듈은 아래와 같이 2가지 모듈이 있습니다.
- Repository
- DataSource
출처 : [안드로이드 공식 홈페이지 DataLayer]
Repository와 DataSource모듈 모두, Remote Server나 Local Server의 데이터와 깊은 연관이 있을거라 생각이 드시나요? 그렇다면 이 글을 잘 따라오셨다고 생각합니다! 그럼 이제 각 모듈의 책임에 대해 간단히 설명드려볼까 해요!
[Repository]
- UpStream관점에서 앱 내 필요 데이터를 DataSource에 요청하는 책임
- DownStream관점에서 DataSource로부터 받아온 데이터를 새로운 모델로 가공하여 하위 레이어(Domain or Ui)에 전달해주는 책임
- 가공한 데이터를 Repository모듈에 캐싱하는 책임
이러면 와닿지 않으니, 일상적인 이야기로 예시를 들어볼까 해요. 우리가 밖에 나와있는데 너~무 배가 고픈데 저~기 김밥천국이 있습니다! 오히려 좋아!
김밥천국은 어찌보면 Repository라고 볼 수 있습니다. 동의하시나요? Repository는 사전적인 의미로 '저장소'라는 뜻을 가지고 있어요. 그리고 김밥천국은 말 그대로 '분식의 저장소'라고 할 수 있어요.
김밥천국이 제가 말씀드린 위 'Respotory'의 책임과 부합하는지 따져보도록 할까요? 우선 UpStream관점입니다.
우선, 김밥천국(=Repository)은 손님께 요리를 제공하기 위해 음식 재료를 미리 비축해놓을 필요가 있습니다. 그래서 가게 문을 열기 전, 여러 가게(=DataSource)로부터 재료들을 사옵니다.
그리고 우린 배가 너무 고파, 김밥천국으로 후딱 들어가 치즈라면과 김밥을 주문합니다. 여기까지가 UpStream관점에서의 Repository책임입니다. 그럼 이제 DownStream관점의 책임을 볼까요?
식재료를 제공받아 비축해놓은 김밥천국(=Repository)는 우리가 주문한 요리를 만들기 시작합니다. (=데이터 가공) 그리고 우리에게 드디어! 제공해주죠(=하위 레이어들에게 데이터 제공) 이것이 Repository의 책임입니다.
개념적으로 이해했으니, 코드적으로 알아볼 시간입니다 [DataLayer의 책임은 무엇인가]에 자세히 적어놓았으니 참고하시면 좋을것 같아요~ 이제 DataSource의 책임에 대해 알아보도록 하겠습니다.
[DataSource]
- UpStream관점으로 Remote or/and Local Server에 데이터를 요청한다.
- DownStream관점으로 Repository에 데이터를 제공한다.
방금 말씀드린 김밥천국의 예시를 통해 이어서 설명이 가능할것 같아요. 아까 김밥천국에선 손님께 주문을 받고(=UpStream) 음식을 손님께 제공해드렸던 사실(=DownStream)을 기억하시나요?
그리고 김밥천국(=Repository)은 손님께 음식을 제공하기 위해 재료가게(=DataSource)로부터 재료를 주문했으며(=UpStream) 이를 김밥천국에 제공(=DownStream)했습니다.
좀 더 쉽게 말씀드려볼까요? 김밥천국이 만들 음식의 재료를 파는 곳은 1차 생산자라고 볼 수 있습니다. 그리고 김밥천국은 손님께 음식을 제공하는 2차 생산자라고 볼 수 있지요.
[DataSource와 Repository]
따라서 DataSource는 Remote or Local Server로부터 받은 데이터를 1차 생산자로 제공해주는 모듈이라고 볼 수 있고, Repository는 DataSource로부터 받은 데이터를 하위 레이어(Domain or UI)에 제공해주는 2차 생산자의 역할이라고 볼 수 있어요.
이제 간단한 예제코드를 통해 DataSource와 Repository의 사용 사례를 알아보도록 하겠습니다.
data class UserInfoResponse{
@SerializedName("name")
val name: String,
@SerializedName("age")
val age: Int,
@SerializedName("phone_number")
val phoneNumber: String
@SerializedName("address")
val address: String
}
interface BaseApi {
@GET("/v1/api/user")
fun fetchUserInfo(): UserInfoResponse
}
class UserInfoDataSource @Inject(
private val baseApi: BaseApi
) {
fun fetchUserInfo(): Flow<UserInfoResponse> {
return flow {
emit(baseApi.fetchUserInfo())
}
}
}
참조 : [안드로이드 공식 홈페이지 DataLayer]
[김밥천국 예시와 비교해보며]
reponse 데이터를 그대로 반환하는, 어찌보면 raw한 데이터를 그대로 제공해주는 느낌이 들지 않나요?
그리고 이건 마치, 김밥천국에게 '식재료 자체'를 제공해주는 1차 생산자 식자재 가게와 비슷한 느낌이 나지 않으신가요~?
이렇게 반환한 데이터는 Repository에서 가공을 하게 됩니다. 바로, UI에 적절하게 바인딩 되는 형태로 말이죠
class UserInfoRepository @Inject(
private val userInfoDataSource: UserInfoDataSource
) {
fun userInfo = userInfoDataSource.fetchUserInfo().map { response ->
UserInfoUiState(
val name: String,
val age: Int
)
}
}
[김밥천국 예시와 비교해보며]
위의 Repository모듈에서는 DataSource로부터 받은 데이터를 가공하고 있습니다. UI가 필요로 하는 새로운 모델로 말이죠. 그리고 이러한 가공은 마치, 김밥천국이 재료를 손질해가며 손님들이 먹고 싶은 음식을 만드는 과정과 비슷한 맥락같지 않나요?
이렇게 DataLayer의 Repository모듈과 DataSource모듈의 책임에 대해 간단하게 알아봤어요. 좀 더 자세한 코드와 설명을 듣고싶다면, 다음 [DataLayer의 책임은 무엇인가]를 참고해보시면 더욱 도움이 되실거라 생각합니다.
지금까지 안드로이드 클린 아키텍쳐란 무엇인가에 대해 알아보았습니다. 그리고 클린아키텍쳐를 이해하기 위해선 각 레이어들(UI, Domain, Data)의 책임을 명확히 정의할 수 있어야 한다는 것도 알아봤죠. 각 레이어들은 다음과 같은 책임이 있었습니다.
[UI Layer의 책임]
- UI Elements
- UI상태 자체를 나타낸다. (TextView, ConstraintLayout, ComposeUI...)
- UI State Holder(=viewmodel)
- UpStream관점으로 사용자의 이벤트를 받아 비즈니스 로직을 실행시키는 책임
- DownStream관점으로 상위 레이어(Domain or Data)로부터 받은 데이터를 UI에 바인딩해준다.
- UI에 바인딩한 데이터를 상태 홀더 클래스(LiveData, StateFlow)통해 보유한다.
참고 : [안드로이드 공식 홈페이지 UI Layer]
후속 포스팅 : [UI Layer의 책임은 무엇인가]
[Domain Layer의 책임]
- UseCase
- ViewModel의 중복 코드가 정의되는 책임
- ViewModel의 책임 이외에 필요한 비즈니스 로직이 정의되는 책임
참고 : [안드로이드 공식 홈페이지 Domain Layer]
후속 포스팅 : [Domain Layer의 책임은 무엇인가]
[Data Layer의 책임]
- Repository
- UpStream관점에서 앱 내 필요 데이터를 DataSource에 요청하는 책임
- DownStream관점에서 DataSource로부터 받아온 데이터를 가공하여 하위 레이어(Domain or Ui)에 전달해주는 책임
- 가공한 데이터를 Repository모듈에 캐싱하는 책임
- DataSource
- UpStream관점으로 Repository가 필요한 데이터를 Remote or/and Local Server에 요청한다.
- DownStream관점으로 Repository에 데이터를 제공한다.
좋은 글 감사합니다!