레이어를 분리하는 이유
코드를 여러 레이어로 분리하면 앱의 확장성이 높아지며 앱이 더 견고해지고 테스트하기 더 쉬워진다. 또한 경계가 명확히 정의된 여러 레이어를 사용하면 여러 개발자가 서로에게 부정적인 영향을 주지 않고 동일한 앱을 더 쉽게 작업할 수 있다.
Android의 권장 앱 아키텍처에는 앱에 최소한 UI 레이어와 데이터 레이어가 반드시 있어야 한다고 명시되어 있다.
데이터 레이어 정의
데이터 레이어는 앱의 비즈니스 로직과 앱 데이터 소싱 및 저장을 담당한다. 데이터 레이어는 단방향 데이터 흐름 패턴을 사용하여 UI 레이어에 데이터를 노출한다. 데이터는 네트워크 요청, 로컬 데이터베이스, 기기의 파일 등 여러 소스에서 가져올 수 있다.
앱에 데이터 소스가 두 개 이상 있을 수도 있다. 앱이 열리면 첫 번째 소스인 기기의 로컬 데이터베이스에서 데이터를 검색한다. 앱은 실행되는 동안 두 번째 소스에 네트워크를 요청하여 최신 데이터를 가져온다.
UI 코드와 별도의 레이어에 데이터를 배치하면 코드의 한 부분에서 변경해도 다른 부분에 영향을 주지 않는다. 이 접근 방식은 관심사 분리라는 디자인 원칙의 일부이다. 코드의 한 섹션은 자체 관심사에 초점을 맞추며 내부 작동 정보를 다른 코드로부터 별도로 캡슐화한다. 캡슐화는 내부적으로 코드가 작동하는 방식을 코드의 다른 섹션으로부터 숨기는 방식이다. 코드의 한 섹션이 다른 섹션과 상호작용해야 하는 경우 인터페이스를 통해 처리한다.
데이터 레이어는 하나 이상의 저장소로 구성된다. 저장소 자체에는 0개 이상의 데이터 소스가 포함된다.
권장사항에 따르면 앱에 사용되는 데이터 소스 유형별로 저장소가 있어야 한다.
저장소 정의
- 일반적으로 저장소 클래스는 다음을 실행한다.
- 앱의 나머지 부분에 데이터 노출
- 데이터 변경사항을 한곳으로 일원화
- 여러 데이터 소스 간의 충돌 해결
- 앱의 나머지 부분에서 데이터 소스 추상화
- 비즈니스 로직 포함
튜토리얼에서 중점으로 다루는 Mars Photos 프로젝트에는 단일 데이터 소스 (네트워크 API 호출)가 있다. 데이터를 가져오기만 하므로 비즈니스 로직은 없다. 데이터는 데이터 소스를 추상화하는 저장소 클래스를 통해 앱에 노출된다.
먼저 저장소 클래스를 만들어야한다. Android 개발자 가이드에서는 저장소 클래스의 이름이 관련 데이터에 따라 저장된다고 한다. 저장소 이름 지정 규칙은 데이터 유형 + 저장소이다. 이 앱의 경우 MarsPhotosRepository가 된다.
저장소 만들기
package com.example.marsphotos.data
import com.example.marsphotos.network.MarsApi
import com.example.marsphotos.network.MarsPhoto
interface MarsPhotosRepository {
suspend fun getMarsPhotos(): List<MarsPhoto>
}
class NetworkMarsPhotosRepository(): MarsPhotosRepository {
override suspend fun getMarsPhotos(): List<MarsPhoto> {
return MarsApi.retrofitService.getPhotos()
}
}
인터페이스로 저장소를 만들었다면 인터페이스 내부에 추상 함수를 추가한다. 이 함수는 marsPhoto 객체 목록을 반환하고, 코루틴에서 호출되므로 suspend로 선언한다.
이후, 인터페이스 아래에 인터페이스를 구현하기위한 클래스를 만들고, 상속받은 메서드를 재정의한다. 이 함수는 MarsApi.retrofitService.getPhotos() 호출을 통해 데이터를 반환한다.
이름의 retrofitService만 봐도 알겠지만 네트워크 API 통신을 통해 데이터를 받아오고, 이 네트워크 IO 작업은 이전에 공부한 대로 코루틴을 사용해 비동기식으로 진행하여 사용자의 경험을 증대시킨다.
https://small-stepping.tistory.com/949
package com.example.marsphotos.ui.screens
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.marsphotos.data.NetworkMarsPhotosRepository
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
sealed interface MarsUiState {
data class Success(val photos: String) : MarsUiState
data object Error : MarsUiState
data object Loading : MarsUiState
}
class MarsViewModel : ViewModel() {
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
private set
init {
getMarsPhotos()
}
private fun getMarsPhotos() {
viewModelScope.launch {
marsUiState = try {
val marsPhotosRepository = NetworkMarsPhotosRepository()
val listResult = marsPhotosRepository.getMarsPhotos()
MarsUiState.Success("Success: ${listResult.size} Mars photos retrieved")
} catch (e: IOException) {
MarsUiState.Error
} catch (e: HttpException) {
MarsUiState.Error
}
}
}
}
ViewModel 내부에 있는 getMarsPhotos의 코루틴을 통해 저장소의 구현된 추상 메서드와 연결된다. 기존에 작성된 코드에선 ViewModel의 getMarsPhotos() 코루틴 내부에 바로 네트워크 요청을 실행시켰으나, 저장소를 만들고 ViewModel이 저장소와 연결되어 저장소에서 네트워크 요청을 하게 만든다.
ViewModel이 더이상 데이터의 네트워크 요청을 직접 실행하지 않고, 저장소가 데이터를 제공한다. ViewModel은 더 이상 MarsApi 코드를 직접 참조하지 않게된다.
이러한 접근 방식은 데이터를 가져오는 코드가 ViewModel에서 느슨하게 결합되도록 만드는 데 도움이 된다. 느슨한 결합은 저장소에 getMarsPhotos()라는 함수가 있는 한 다른 항목에 부정적인 영향을 미치지 않고 ViewModel 또는 저장소를 변경할 수 있다.
이제 호출자에 영향을 주지 않고 저장소 내부의 구현을 변경할 수 있다. 대규모 앱의 경우 이 변경으로 여러 호출자를 지원할 수 있다.
'개발 > 안드로이드' 카테고리의 다른 글
로컬 테스트 설정 (0) | 2024.05.23 |
---|---|
종속 항목 삽입 (0) | 2024.05.23 |
코루틴 동기와 비동기, 코루틴 용어 (0) | 2024.05.21 |
적응형 앱, 반응형 UI 탐색 (1) | 2024.05.16 |
탐색 그래프 없이 화면 변경 (1) | 2024.05.16 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!