Architecture: The data layer — MAD Skills[번역]

DwEnn
10 min readApr 6, 2022

--

📝 NOTE : 이 글은 영상의 스크립트를 번역한 것이며 오역이 있을 수 있습니다. 😅

이번 MAD(Modern Android Development) Skills 에피소드에서는 앱 아키텍처를 위한 시리즈를 다룹니다. 그러면Data Layer 의 기초부터 시작하겠습니다. 👀

App Architecture

앱 아키텍처의 맨 아래에Data Layer 를 그리면 Data Layer 에는 애플리케이션 데이터와 비즈니스 로직이 포함됩니다. 비즈니스 로직은 애플리케이션 데이터의 생성, 저장, 변경 방법을 결정하여 앱에게 제공합니다. Data Layer 는 각각 0개 이상의 Data Source 와 상호작용할 수 있는 Repository 들로 구성됩니다. Data Source는 이름 그대로, 앱이 필요한 데이터와 function 을 제공합니다.

DataSource는 네트워크와 로컬 데이터베이스 파일, 메모리 데이터를 제공할 수 있습니다. 그리고 일반적으로 비즈니스 로직의 한 단위를 유지하는 하나의 데이터 소스로만 동작할 책임을 가집니다.(예: articles, users, moives)

Repository 클래스는 앱의 모든 계층에서 Data Layer 와 상호작용하는데 사용됩니다. 데이터를 앱의 나머지 부분에 노출시키는 것은 물론, 데이터 변경 사항을 중앙 집중화하고, 여러 DataSource 간의 충돌을 해결하고, 비즈니스 로직을 포함합니다.

One repository per data type

여러분은 여러분의 앱에서 다루는 각각의 다른 종류의 데이터에 대해 Repository 클래스를 만들어야합니다. 예를 들어, movies 에 관련된 데이터를 위한 MoviesRepository 를 만들거나 payments 에 관련된 PaymentsRepository 클래스를 만들 수 있습니다. Repository 는 서로 다른 DataSource 를 결합할 수 있으며, 서로 간의 잠재적인 충돌을 해결하는 역할을 합니다. 만약 로컬 데이터베이스의 데이터와 서버 간의 충돌이 있을 경우 Repository 에서 이를 감지하고 수정해야 합니다.

class MoviesRepository(...)class PaymentsRepository(...)

Exposing data and receiving events

앱의 모든 계층이 DataSource 에 직접 의존해서는 안된다는 것을 명심해야합니다. Data Layer 의 진입 지점은 항상 Repository 클래스입니다. Repository 패턴의 일반적인 사용법은 CRUD(create-read-update-delete)에 대한 원샷 호출을 수행하는 것입니다. 이것들은 Kotlin 에서 suspend function 으로 구현될 수 있습니다. 또한 데이터 스트림을 노출하여 시간 경과에 따른 데이터 변화에 대한 알림을 받을 수 있습니다. (예: Kotlin flow) 이것은 이 비디오의 범위 밖이므로, 만약 더 많이 배우고 싶다면 이곳의 flow 가이드를 확인하세요.

Repository 에서 여러 DataSource 를 처리하는 것이 까다로울 수 있습니다. 여러분은 소스의 진실(Source of truth)을 선택하고 항상 일관된 상태에 있도록 해야합니다.

Source of truth

구체적인 예를 확인해보겠습니다. news 를 위한 두 가지 DataSource 를 가지고 있다고 가정하겠습니다. 로컬 news 데이터 원본은 Room 데이터베이스에 의존하여 article 목록을 반환하거나 업데이트할 수 있습니다.

RemoteNewsDataSource 는 apiClient 에 의존합니다. (이 Retrofit client 는 news 데이터만 가져올 수 있습니다)

class NewsRepository {
val localNewsDataSource: LocalNewsDataSource,
val remoteNewsDataSource: RemoteNewsDataSource
) {
suspend fun fetchNews(): List<Articles> {
try {
val news = remoteNewsDataSource.fetchNews()
localNewsDataSource.updateNews(news)
} catch (exception: RemoteDataSourceNotAvailableException) {
Log.d("NewsRepository", "Connection failed, using local data source")
}
return localNewsDataSource.fetchNews()
}
}

NewsRepository 는 이 두 DataSource 에 의존합니다. 만약 다른 계층에서 news 를 가져오길 원한다면 우리는 Repository 에서 fetchNews 메소드를 사용합니다. 이 메서드는 먼저 네트워크로부터 news 를 로드합니다. 성공하면 로컬 데이터베이스를 업데이트 합니다. 만약 데이터 연결이 없거나 서버가 다운되어 오류가 발생하여 실패한다면 UI 에 무언가를 표시하는 대신 로그를 출력합니다. 마지막으로, 이전 실행결과가 어찌되었든 데이터베이스에서 결과를 반환 합니다. 그것이 우리 소스의 진실(Sourth of truth)이기 때문입니다.

이 사례는 사용자가 편집할 수 없는 데이터를 사용하기 때문에 매우 간단합니다. 하지만 충돌을 해결하는 것은 어려울 수 있습니다. 예를 들어, 두 명의 사용자에 의해 동시에 일정이 수정되는 캘린더 앱을 상상해보겠습니다. 이 작업은 약간의 고민이 필요합니다. 🤔
오프라인환경에서 첫 번째 사용자 경험을 제대로 구현하려면 이 작업을 올바르게 수행하는 것이 중요합니다. 원격 api 를 사용하는 라이브러리와 파일 스토어와 같은 원격 데이터베이스에는 사용자가 사용할 수 있는 캐싱 메커니즘이 있으며 충돌도 처리할 수 있습니다.

Immutability

이제 불변성에 대해 이야기해 보겠습니다. Data Layer 에 의해 노출된 데이터는 모든 클래스가 데이터를 변조할 수 없도록 변경이 가능해야 합니다. 그렇지 않으면 값이 일관되지 않은 상태에 놓이게 되는 리스크를 가지게 됩니다. 불변 데이터의 또 다른 장점은 여러 스레드에 의해 안전하게 처리될 수 있다는 것입니다.

Kotlin 의 data class 는 이를 위한 완벽한 툴입니다. 그런데 엔티티를 모델링할 때 데이터베이스 또는 원격 API 에서 반환되는 모델이 다른 계층에 필요한 모델이 아닐 수 있다는 점을 고려해야 합니다. 위 ArticleApiModel 을 보겠습니다. modifications 또는 authorDateOfBirth 가 필요하지 않은 경우 UI Layer 를 위한 다른 모델을 생성하는 것이 좋습니다. 이것은 여러분의 코드를 더 깔끔하게 만들 뿐만 아니라 각 계층이 필요로 하는 모델을 정의할 수 있도록 더 나은 관심사 분리를 제공합니다.

Threading

이제 스레딩에 대해 이야기해 봅시다. 🤗
DateSourceRepository 는 메인 스레드에서 호출되어 안전히 저장되어야합니다. 즉, DateSource 또는 Repository 는 장기간 실행되거나 blocking 작업을 수행할 때 적절한 스레드로 로직 실행을 이동하는 역할을 합니다.

suspend fun fetchDataFromNetwork() {
withContext(Dispatchers.IO) {
fetch()
}
}

📝 NOTE : 백그라운드 처리 가이드를 참고

Errors

또 다른 고려사항으로 데이터 작업이 항상 성공하는 것이 아니기 때문에 에러인 경우를 살펴보아야합니다. 실패에 대한 정보를 다른 계층에 전파하는 것이 중요합니다. 하나의 방법으로 단순히 예외가 suspend function 과 함께 전파되도록하는 것입니다. UI Layer 또는 Domain Layer 의 try-catch 블록에서 Repository 호출을 감싸거나 flow 를 사용하는 경우 catch 연산자를 사용할 수 있습니다.

// use try-catch
try {
moviesRepository.setFavorite(id, isFavourite)
} catch (exception: Exception) {
// handle exception
}
// use flow
movies.catch { exception ->
// handle exception
}.collect {
// collect data
}

또 다른 방법은 Data Layer 에서 이러한 오류를 포착하고 보다 의미 있는 예외와 함께 성공 또는 실패를 포함할 수 있는 데이터를 노출하는 것입니다. 어떤 경우에도 오류가 발생할 경우 대처하는 것을 잊지 않아야 합니다.

Multiple levels of repositoires

앞에서 보았듯이 Repository 는 여러 DataSource 에 의존할 수 있습니다. 경우에 따라서는 여러 수준의 Repository 가 필요할 수 있습니다. 이건 걱정할 필요가 없습니다. 이 예에서 UserRepositoryLoginRepositoryRegistraionRepository 를 필요로 합니다. DataSource 를 공유할 수 있는것과 비슷합니다.
이 모든 팁은 대부분의 앱에서 잘 작동해야 하는 권장사항이며 만약 여러분이 따르지 않을 이유가 있다면 그렇게 하시면 됩니다.

Testing repositories

마지막으로 테스트에 대해 이야기해보겠습니다. Data Layer 는 일반적으로 테스트하기 쉽습니다. Repository 는 보편적인 Unit Test 가 이루어집니다. 여러분은 DataSource 를 가짜 또는 mocks 로 바꾸고 Repository 가 데이터를 올바르게 처리하고 필요로하는 DataSource 를 호출하고 있는지 확인합니다.

DataSource 를 테스트하는 것은 데이터베이스나 api client 에 의존하고 있기 때문에 약간 까다로울 수 있습니다. 하지만 보통 라이브러리들은 테스트를 위한 테스트 아티팩트나 매커니즘을 제공해줍니다. 예를 들어, RoomDataSource 를 테스트하는데 도움이 될 수 있는 InMemoryDatabase 구현을 제공합니다.

Big tests

end-to-end 또는 빅 테스트에서는 앱의 모든 계층을 동시에 테스트합니다. 하지만 가짜 데이터를 사용하여 테스트를 더 빠르고 신뢰가능하게 만들 수도 있습니다. DI(종속성 주입)을 사용하는 경우 DataSource 또는 Repository 를 가짜 구현체들로 바꾸어야합니다. 또한 wiremock 이나 mock web server 같은 인기 라이브러리로 네트워크 호출을 위조할 수 있습니다. (Android의 종속 항목 주입)

이 비디오는 아키텍처 가이드의 Data Layer 글을 요약한 것입니다. Data Layer 의 모범 사례에 대한 코드 샘플과 자세한 내용은 이 가이드의 나머지 부분을 확인하세요.

--

--