Modern App Architecture
얼마전 Google 에서 권장하는 새로운 가이드 라인 Architecture가 발표 되었습니다.
정식적인 네이밍은 없는 것으로 보이나 Google이 Modern App Architecture 라고 지어 놓은 것으로 보고 이렇게 제목으로 적어 놓았습니다.
지난 시간에 Clean Arhcitecture에 대해서 적어 놨는데 사실 오늘 설명 할 아키텍처를 좀 더 와닿게 설명 하고 싶어서 적어 놨습니다.
바로 시작해 보겠습니다.
Clean Arhcitecture는 UI(presentor) -> Domain <- Data (각 레이어가 서로 의존하는 구도 입니다.) 으로 Layer 가 서로 의존 하고 있는 것을 확인 할 수 있습니다.
Presentor는 사용자에게 보여질 영역
Domain은 UI에게 보여질 Model, Repository, UseCase 영역
Data는 Domain의 모델을 위해 Data영역의 모델을 가공하여 UseCase에게 제공하는 영역(비즈니스 로직을 담당)
→ Domain 영역은 그 어떤 영역에게 의존하지 않고 매우 독립적인 영역 입니다.
Clean Architecure 는 위 총 3가지의 영역으로 구성되어 있는 아키텍처 입니다.
Clean Architecture 를 공부했을때만 해도 구글에서 권장하는 아키텍처 가이드 라인이었습니다만 변경된것으로 보입니다.
다음은 본론인 Modern App Architecture 입니다.
Modern App Architecture의 각 영역을 정리 하자면
UI는 사용자에게 보여질 영역
Data는 앱의 비즈니스 로직을 구성하는 영역 여기까지는 필수 입니다.
Domain은 UI와 Data 레이어 간의 상호작용을 간소화 하고 재사용하기 위한 영역입니다. (UseCase)
즉, 매우 독립적인 영역 이었던 Domain 영역이 이제 필요할때만 채택해서 아키텍처를 설계하면 됩니다.
UI 영역에는 동일하게 Activity, Fragment, ViewModel 등이 존재하면 되며, Data 영역에는 UI에게 제공할 Repository가 존재 하면 됩니다. Repository에는 Database, API를 주입하면 되며, 이 두가지를 은닉화 하여 제공하는 DataSource를 주입 하여도 됩니다.
즉, 비즈니스 로직을 담당하면 됩니다.
코드레벨로 적어보겠습니다.
// Repository의 예시
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
// Coroutine을 활용한 DataSource의 예시
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
withContext(ioDispatcher) {
newsApi.fetchLatestNews()
}
}
}
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
각 의존성 역시 상황에 맞게 주입 하면 되며, Google 에서는 Hilt는 적극 권장 하고 있습니다.
다음은 Data 영역에서 사용가능한 패키지 트리 입니다.
data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/
Database를 담당 하는 local 패키지, Network 작업할 network 패키지, UI에게 제공할 Model 패키지, Repository 등을 담당하고 있는 것을 알 수 있습니다.
// Data에서 사용가능한 Model의 예시
@Serializable
data class NetworkAuthor(
val id: String,
val name: String,
val imageUrl: String,
val twitter: String,
val mediumPage: String,
val bio: String,
)
@Entity(tableName = "authors")
data class AuthorEntity(
@PrimaryKey
val id: String,
val name: String,
@ColumnInfo(name = "image_url")
val imageUrl: String,
@ColumnInfo(defaultValue = "")
val twitter: String,
@ColumnInfo(name = "medium_page", defaultValue = "")
val mediumPage: String,
@ColumnInfo(defaultValue = "")
val bio: String,
)
다음은 Domain Layer 입니다.
두 아키텍처의 큰 차이라고 볼 수있는 Domain는 이해를 돕기 위해서 Clean Architecture 기반의 UseCase 부터 적어 보겠습니다.
// Domain 영역에 존재하는 UseCase
class FormatDateUseCase(
private val formatDateRepository: FormatDateRepository
) {
operator fun invoke(date: Date): String {
return formateDateRepository.getFormatDate(date)
}
}
// Domain 영역에 존재하는 Repository
interface FormatDateRepository {
fun getFormatDate(date: Date): String
}
// Data 영역에 존재하는 Repository
class FormatDateRepositoryImpl: FormatDateRepository {
overide getFormatDate(date: Date): String {
/**/
}
}
위 예제 코드는 Repository를 구현하는 구현체 입니다.
Data영역의 Repository의 구현체가 Domain 영역을 의존 하는 것을 확인 할 수 있습니다.
여기서 Clean Architecture와 Modern Architecture의 차이를 알 수 있습니다.
다음은 Modern Architecture의 예시 입니다.
// Domain 영역에 존재하는 UseCase
class FormatDateUseCase(
private val formatDateRepository: FormatDateRepository
) {
operator fun invoke(date: Date): String {
return formateDateRepository.getFormatDate(date)
}
}
// Data 영역에 존재하는 Repository
interface FormatDateRepository {
fun getFormatDate(date: Date): String
}
혹은
// Data 영역에 존재하는 Repository 구현체
class FormatDateRepositoryImpl: FormatDateRepository {
overide getFormatDate(date: Date): String {
/**/
}
}
여기서 Repository의 존재 위치가 변경 되었습니다.
Clean Architecture의 Repository는 Domain 영역에 존재하고 Domain의 UseCase가 Repository를 필요로 하기 때문에 Domain은 어떤 영역에도 의존하지 않습니다.
그에 반면 Modern Architecture는 Repository가 Data 영역이 담당 하고 있기 때문에 Domain의 UseCase가 Data를 의존 하게 됩니다.
사실 ViewModel 입장에서는 Clean Architecture 이든 Modern Architecture 이든 항상 UseCase 를 필요로 합니다. 때문에 Clean Architecture나 Modern Architecture 에서 Presentor는 항상 Domain을 의존하고 있습니다.
그래서 결론은 Repository의 위치에 따라서 각 레이어별 의존하는 구도가 완전히 달라지게 됩니다.
이 뿐만이 아닙니다.
Clean Architecture 상으론 Data의 Model과 Domain Model이 서로 매핑해주는 작업을 Repository 에서 작업을 해줘야 했습니다만,
Modern Architecture 에서는 공식 문서에 따르면 Domain이 별도의 Model을 가지고 있지 않아 따로 매핑 작업에 대해서는 별 다른 언급이나 레퍼런스가 없습니다. (하지만 ViewModel 에서 LiveData를 사용할 경우 observing할 Model을 알아야하고 UI → Domain(Optional) → Data 의 의존 구도에 따르면 Mapping 작업이 추가적으로 필요 할 것으로 판단 됩니다.)