Data 계층
- 데이터 계층은 앱 데이터의 생성과 저장 및 변경 로직에 관련된 계층이다.
-> 데이터 계층의 관심사는 앱 데이터를 생성/저장/변경/삭제 하는것에만 맞춰져있다. - 앱 데이터의 CRUD 로직과 관련된 계층이라는 뜻이다.
- CRUD는 Create, Read, Update, Delete의 약자이다.
Data 계층의 아키텍처
Data 계층을 구성하는 계층
- Repository 계층
- Datasource 계층
Repository 계층과 DataSource 계층의 관계
- Repository 계층은 DataSource 계층을 참조한다.
- Repository 계층에서 DataSource 계층의 코드를 호출한다는 것이다.
-> UI 계층에서 처리할 수 없는 비즈니스 로직 처리가 필요하거나 앱 데이터 생성/저장/변경 처리가 필요한 경우에 UI 계층에서 데이터 계층의 Repository를 호출하고, Repository는 DataSource를 호출하는 구조다.
- 데이터 계층은 비즈니스 로직을 포함하기도 한다.
- 도메인 계층을 두어 별도의 비즈니스로직을 처리할 수 있지만, 이는 필수 계층이아니다.
- 도메인 계층을 별도로 둘만큼 비즈니스 로직이 많지 않을 때에는 비즈니스 로직을 데이터 계층에서 같이 처리해도 된다.
DataSource 클래스가 담당하는 일 : CRUD 작업
- DataSource 클래스에서는 앱 데이터를 생성/읽기/변경/삭제하는 작업을 구현해야 한다.
- CRUD에서 Create 작업의 예시: 파일, 로컬 디비, 네트워크 통신
- 앱 데이터를 Create(=생성)할 수 있는 작업은 여러가지가 있다;
- '파일에 저장되어 있는 데이터를 읽어오기' 또는 '로컬 데이터 베이스에 저장된 데이터 읽어오기' 또는 '네트워크 통신을 통해 외부(서버)로부터 데이터를 얻어오기'등의 작업이 해당된다.
- DataSource 클래스 구현 시 주의점
- 하나의 DatSource 클래스는 반드시 하나의 데이터 출처에서 데이터를 가져오도록 구현해야한다.
- 로컬 DB로부터 데이터를 가져오는 코드가 작성될 DataSource 클래스는 'LocalDataSource' 클래스이고, 네트워크 통신을 통해 외부로부터 데이터를 가져오는 코드가 작성될 DataSource 클래스는 'RemoteDataSource' 클래스이다.
- DataSource 클래스 이름 컨벤션
- 데이터 타입 + 데이터 출처 + 'DataSource' 로 짓는다.
- 예시 - 보통은 'NewsRemoteDataSource', 'NewsLocalDataSource' 등으로 짓는다.
- 데이터 출처를 좀 더 명확하게 하고 싶을 때는 'NewsNetworkDataSource' , ' NewsDiskDataSource' 등으로 짓는 경우도 있다.
Repository 클래스가 담당하는일
- DataSource 클래스가 CRUD 작업만 담당한다면, Repository 클래스는 여러가지 일을 담당한다.
- DataSource 클래스를 참조해서 앱 데이터를 Repository로 가져오고, 다른 계층(UI계층이나 Domain계층)에 가져온 데이터를 제공해준다.
- DataSource 클래스로부터 얻은 데이터를 다른 계층에서 사용하기 쉽게 변형한다.
- 서로 다른 DataSource 클래스로부터 가져온 데이터가 충돌을 일으킬 경우, 충돌을 해결한다.
- 비즈니스 로직을 포함한다.
Repository 클래스 구현 시 주의점
- Repository 클래스는 앱에서 다루는 앱 데이터의 종류에 따라 서로 다른 레포지토리 클래스로 생성해야 한다.
- 예시 - 영화 관련 데이터를 다루는 레포지토리는 'MoviesRepository' 클래스로 생성하고, 영화 티켓 결제 관련 데이터를 다루는 레포지토리는 'PaymentsRepository'클래스로 생성하자.
class MoviesRepository(
private val remoteDataSource: RemoteDataSource,
private val localDataSource: LocalDataSource
){
...
}
영화에 대한 데이터를 처리하기 위한 하나의 'MoviesRepository' 클래스지만,
데이터를 가져오는 출처는 'RemoteDataSource'와 'LocalDataSource' 두 곳(서버와 로컬DB)에서 가져오도록 구현할 수 있음을 알 수 있다.
Repository 클래스 이름 컨벤션
- 데이터 타입 + 'Repository' 로 짓는다.
- 예시 - 'MoviesRepository', 'NewsRepository', 'PaymentsRepository' 등으로 짓는다.
- 여기서 데이터 타입이란, 해당 Repository 클래스에서 다룰 데이터를 나타내는 한단어! 정도로 생각하면 될 듯 하다.
Repository 계층의 멀티 레벨
Repository가 또 다른 Repository 를 참조하는 경우
- 보통은 하나의 Repository 클래스가 바로 DataSource 클래스를 참조한다.
- 그러나 하나의 Repository 클래스가 여러 개의 또 다른 Repository 클래스를 한번 더 참조할 수도 있다.
- 하나의 Repository 클래스에서 다루는 데이터 타입이 서로 다른 DataSource로 부터 얻어지는 경우에 이런식으로 구현하면 좋다.
Repository 클래스의 중요성
- 하나의 Repository가 서로 다른 DataSource로부터 데이터를 얻는 경우
- 하나의 Repository가 서로 다른 DataSource로부터 데이터를 얻는 경우가 있다.
- 예시 - 오프라인 상황에서도 사용자에게 데이터를 제공하기 위해 Repository에서 LocalDataSource와 RemoteDataSource 모두에서 데이터를 얻고 있는 경우가 있을 수 있다.
- 여러 개의 DataSource로부터 데이터를 얻을 경우 발생 가능한 문제: 데이터 충돌
- 두개의 DataSource에서 얻은 데이터가 일치하지 않는 경우가 발생할 수 있다.
- 이런 경우를 '데이터 충돌'이러고 한다.
- 예시 - DB에는 특정 게시물에 좋아요 했다고 저장되어 있는 Remote에는 서버쪽의 이슈로 해당 게시물에 좋아요를 누르지 않았다고 저장되어 있는 경우
- Repository 클래스의 중요한 역할
- 데이터 충돌이 있을 경우 Repository 클래스에서 충돌을 해결해야한다.
- Domain 계층이나 UI 계층에서 앱 데이터를 얻기 위해 Repository를 참조할텐데 이 때 Reposiotry는 충돌 해결 완료된 데이터를 제공해야 한다.
Data 계층과 쓰레드
- main-safe를 지켜야한다.
- Repository나 DataSource에서 수행되는 작업들은 main-saf 해야 한다.
- main-safe 란? 메인 쓰레드가 제 역할을 다하도록 메인 쓰레드를 Blocking 하지 않아야 한다는 뜻이다.
- 시간이 오래 걸리는 작업이기 떄문이다
- 보통 Repository와 DataSource에서 수행하는 일은 외부 출처로부터 데이터를 가져오거나 가져온 데이터를 충돌 해결하여 다른 계층에 제공하기 쉬운 형태로 변환하는 작업이다.
- 그래서 시간이 오래 걸린다.
- Repository와 DataSource는 시간이 오래 걸리는 로직들을 메인 쓰레드에서 수행하는 것이 아니라 적절히 다른 쓰레드에서 수행되도록 해야 하는 책임이 있다.
- Coruoutine
- 코틀린 유저들은 보통 코루틴을 사용해서 시간이 오래 걸리는 작업은 다른 쓰레드에서 수행되도록 구현하면 된다.
Data 계층과 라이프 사이클
Data 계층 클래스 인스턴스들의 특징
- Repository 클래스나 DataSource 클래스의 인스턴스는 가능한 오래 메모리에 남아있는 것이 좋다.
- 왜냐하면 앱의 여러 화면이나 여러 부분에서 앱 데이터를 얻기 위해 여기저기서 참조하기 때문이다.
- 그래서 인스턴스를 한 번 만든 후에는 메모리에 오래 유지시켜서 여러 곳에서 재사용할 수 있도록 하는 것이 좋다.
- 참조하려고 했는데 메모리에 없어서 그제서야 인스턴스를 생성해야 한다면 그 만큼 데이터를 얻는 속도가 느려저서 앱 전반적인 속도가 느려질 것이다.
Data 계층 클래스 인스턴스들의 수명 주기
- 인스턴스가 메모리에 생성되어 제거되는 주기를 클래스 인스턴스의 '수명 주기'라고 표현한다.
- 만약 특정 인스턴스를 앱이 실행되는 동안 전반적으로 계속 재사용해야 한다면 해당 인스턴스의 수명 주기를 Application scope로 지정하면 된다.
- 만약 특정 인스턴스가 특정 flow에서만 재사용 할 예정이라면 해당 인스턴스의 수명 주기를 특정 flow scope로 지정해주면 된다.
Data 계층에서 사용하는 Model
외부 데이터 출처로부터 가져온 데이터를 그대로 쓸 수는 없다.
- 보통 대부분의 경우에서는 외부 데이터 출처로부터 가져온 그대로 사용하기는 어렵다
- 예시 - DataSource 클래스에서 서버로부터 특정 뉴스 기사의 정보를 얻어오는 NewsApi를 호출하여 뉴스 기사 데이터를 얻어 왔다고 가정하자. 앱 UI에는 뉴스 제목과 뉴스 내용만 보여주면 된다. 하지만 얻어온 데이터는 아래와 같이 사용하지 않는 더 많은 정보들이 있을 수 있다.
data class NewsApiModel(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val modifications: Array<ArticleApiModel>,
val comments: Array<CommentApiModel>,
val lastModificationDate: Date,
val authorId: Long,
val authorName: String,
val authorDateOfBirth: Date,
val readTimeMin: Int
)
//실제로 사용할 데이터는 title과 content 뿐이다.
Repository 클래스에 필요한 데이터만 간추린 새로운 Model을 만들기
- Repository 클래스에서는 DataSource 클래스에서 얻어온 위와 같은 NewsApiModel을 필요한 데이터만 간추린 새로운 Model로 변환하는 작업을 해주는 것이 좋다.
- 변환된 새로운 Model을 Domain 계층이나 UI 계층에 제공하면 된다.
data class News(
val id: Long,
val title: String,
val content: String,
)
//실제로 사용할 데이터로만 간추러졌다.
새로운 Model로 변환하는 방법의 장단점
- DataSource 클래스에서 외부로부터 데이터를 얻을 때 사용하는 Model 클래스와 Repository 클래스에서 다른 계층에 데이터를 제공할 때 사용하는 Model 클래스를 별도로 두면 클래스가 많아지는 단점이 있기는 하다.
- 하지만 DataSource 클래스와 Repository 클래스의 관심사가 분리된다는 관점에서는 좋은 방법이다.
- DataSource 클래스에서는 CRUD만 담당하고, Repository 클래스에서는 데이터 변환만 담당한다.
- 데이터 출처로부터 얻은 데이터의 형태가 앱에서 실제로 사용할 데이터의 형태와 다른 경우에는 Model 클래스를 분리하는 것을 권장하고 있다.
Data 계층에서 에러 처리하기
에러 발생 가능성이 높은 Data 계층
- DataSource 클래스에서 외부 데이터 출처로부터 데이터를 얻는 과정이 성공적으로 완료될 수도 있지만 실패할 수도 있다.
- 이런 경우에는 에러가 발생하게 된다.
try catch 문으로 에러 처리하기
- 에러가 발생할 경우를 대비하여 try catch문을 사용하면 된다.
- Data 계층에서 에러를잘 처리하여 UI 계층에 알려준다면 UI 계층에서 에러 발생에 적절히 대응할 수 있게 될 것이다.
커스텀 Exception 사용하기
- Data 계층에서는 다른 계층에 에러가 발생했음을 알려줄 때 조금 더 커스텀한 Exception으로 알려주면 더 좋다.
- 예시 - 특정 뉴스가 서버에 존재하지 않는 경우에는 'NotFoundNewsExeption'라는 커스텀 Exception으로 UI 계층에 에러에 대해 더 정확하게 알려줄 수 있다.
Data 계층을 구현할 때 권장 코드
예시 - 네트워크 통신을 통해 데이터를 가져올 떄
- 대부분의 앱은 서버로부터 데이터를 가져오는 경우가 많고 이 때 네트워크 통신을 요청하게 된다.
예시 - 유저가 화면에 진입할 때마다 최신 뉴스 기사를 보여주기
- 최신 뉴스 기사는 서버로부터 얻어와야 한다.
- 서버로부터 데이터를 가져오는 로직이 포함된 'NewsRemoteDtaSource' 클래스와 가져온 데이터를 다른 계층에 제공하기 위한 로직이 포함된 'NewsRepository' 클래스를 구현해야 할 것이다.
- 화면에 진입할 때마다 서버로부터 최신 뉴스 데이터를 가져오기 위해 네트워크 통신을 요청해야 한다.
<NewsRemoteDataSource 클래스 구현하기>
// News 와 관련된 API들을 정의하는 인터페이스
// 인터페이스를 만드는 이유는 API client 구현체를 추상화하기 위해서이다.
// 만약 Retrofit 라이브러리를 사용하던 중, HttpURLConnection으로 클라이언트를 바꿔야 할 경우, 구현체 코드만 수정하면 된다.
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
data class NewsHeadline(
...
)
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
// 네트워크 통신을 통해 외부로부터 뉴스 헤드라인 리스트 데이터를 가져오고, 결과 값으로 반환한다.
// IO 쓰레드에서 수행되기 때문에 main-safe하다.
suspend fun fetchLatestNews(): List<ArticleHeadline> =
val newsHeadlines = withContext(ioDispatcher) {
newsApi.fetchLatestNews()
}
return newsHeadlines;
}
<NewsRepository 클래스 구현하기>
// 이 예시는 뉴스 헤드라인을 가져온 후 레포지토리에서 별다른 로직을 추가하지 않은 상황
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
val newsHeadlines = newsRemoteDataSource.fetchLatestNews()
}
<출처>
'Android > Architecture' 카테고리의 다른 글
안드로이드 Clean Architecture - 예제 (Rxjava) (0) | 2023.05.27 |
---|---|
안드로이드 Clean Architecture - 개념 (0) | 2023.05.27 |
[Android] 앱 아키텍처 가이드 (4) - Domain Layer (0) | 2023.05.15 |
[Android] 앱 아키텍처 가이드 (2) - UI Layer (0) | 2023.04.29 |
[Android] 앱 아키텍처 가이드 (1) - 개요 (0) | 2023.04.12 |