제가 지금까지 했던 모든 프로젝트 아키텍쳐 패턴은 MVVM 패턴으로 설계되어있습니다. 하지만 정작 Android developer에서 제공하는 앱 아키텍쳐 가이드를 명확히 숙지하고 있나?라는 의문이 들었습니다. UI Layer / Data Layer / Domain Layer 의 역할을 알고 패키징을 할 수 있으며 ViewModel과 Databinding 등 Android Jetpack에서 제공하는 Architecture 컴포넌트 라이브러리를 사용했지만 정작 '안드로이드에서 제공하는 앱 아키텍쳐 가이드에 대해 설명해봐~'라고 하면 자신없게 대답하는 저는 발견했습니다😥
그래서 해당 포스팅을 통해 깊게 공부하고자 하는 내용은,
본 포스팅을 보고 공부하고자 하는 분이라면, 아예 MVVM 패턴 사용해본 적 없는 분보다 사용한 경험은 있지만 저처럼 개념을 깊이 파고들어보고 싶은 분이 읽으시면 더 도움될 것 같습니다!
혼동되는 용어부터 정리해보겠습니다!
MVVM 아키텍처 인가? 패턴인가?
아키텍처란?
시스템 구성과 동작 원리 등 최상의 소프트웨어를 구성하는 설계도
디자인 패턴이란?
좋은 코드를 설계하기 위한 방법론
결론적으로 MVVM은 "아키텍처 패턴" 이 정확한 표현이라고 할 수 있습니다!
그리고 안드로이드에서 자주 사용되는 '아키텍쳐 패턴'에는 MVC패턴,MVP패턴,MVVM패턴이 있습니다!
안드로이드 공식문서에서 설명하는 '권장하는 아키텍쳐란' MVVM과 같은 아키텍쳐 패턴을 사용하면서, 프로젝트에서 계층을 나눠 관심사를 분리시킨 '설계구조'라고 할 수 있습니다.
위 사진은 안드로이드권장 아키텍쳐+MVVM패턴을 함께 사용한 모습이라고 봐도 좋습니다.
Architecture
컴포넌트로 분류되어있는 라이브러리는 아래와 같습니다
Data Binding
: xml파일에 Data를 연결해서 사용할 수 있게 도와준다
Lifecycles
: 안드로이드 activity 생명주기 관련 유틸리티
LiveData
: 데이터가 변경될때 실시간으로 view에 알려준다
Navigation
: activity, fragment간 이동을 쉽게 도와준다
Paging
: 대량의 데이트를 관리해주는 유틸리티
Room
: Database 보다 쉽게 사용할 수 있게 도와준다
WorkManager
: 백그라운드 작업을 보다 쉽게 도와준다
Architecture
컴포넌트 라이브러리들을 사용하면 구글이 권장하는 안드로이드 아키틱쳐를 자신의 앱 프로젝트에 적절히 적용할 수 있습니다Android Developer 도큐먼트 - 앱 아키텍처 가이드에서는 아키텍쳐를 설명하는 가장 첫문단으로 '모바일 앱 사용자 환경'이라는 개념에 대해 소개하고 있습니다
일반적인 안드로이드 앱은 Activity/Fragment, Service, Content Provider, Broadcast Receiver 와같은 4대 컴포넌트로 구성되어 있습니다. 개발자는 이러한 앱 구성요소 대부분을 Manifest파일에 선언하고 Android OS가 이 manifest 파일을 사용하여 기기를 사용하는 사용자의 전반적인 작업 환경을 망가뜨리지 않으면서 앱을 이 환경에 자연스럽게 통합하는 방법을 결정합니다
여기서 말하는 사용자의 전반적인 작업 환경(모바일 앱 사용자 환경) 이란 무엇을 의미하는 것일까요?
안드로이드 앱을 사용하는 유저는 짧은 시간 내에 여러 앱을 실행하는 경우가 많습니다 따라서 앱이 사용자 중심의 다양한 workflow에 맞게 조정될 수 있어야 합니다.
예를 들어 사용자가 SNS앱에서 사진을 업로드 하는 과정을 생각해보면 SNS앱은 가장 먼저 카메라 인텐트를 트리거 할 것입니다. 그러면 Android OS에서 요청에 따라 카메라 앱을 실행시킵니다. 이 순간 사용자는 카메라 앱으로 이동하게 되면서 SNS 앱에서는 나간 상황이 되지만 사용자의 작업 환경은 끊김없이 연결되어 있는 상태라고 볼 수 있습니다. 그 다음, 카메라 앱에서 앨범을 보기 위해 앨범 인턴트를 트리거할 수 있습니다. 이 후에는 사용자가 다시 SNS앱으로 돌아가서 앨범에서 선택한 사진이나 카메라 앱으로 직접 찍은 사진을 업로드 할 것입니다. 심지어 이때 전화나 알림에 의해 사용 환경의 흐름이 끊어질 수도 있습니다
즉 사용자는 이러한 흐름 중단과 상관없이 다시 SNS앱으로 돌아가 하던 작업을 계속 하길 원할겁니다
이처럼 사용자는 모바일 환경에서 다양한 앱을 시도때도 없이 바꾸기도 하고 전화나 알림 등의 작업도 동시에 하기 때문에 앱에서 이런 사용자 흐름이 중단되지 않고 자연스럽게 흘러가도록 올바르게 처리해야 합니다
이 뿐만 아니라 모바일 기기는 하드웨어 자원(메모리 등)이 제한되어 있으므로 메모리 부족으로 인한 사용자 흐름 중단이 발생할 수도 있습니다
이에 대비해 안드로이드 OS는 메모리 공간이 부족할 경우 '일시정지됨'상태에 있는 앱의 프로세스를 강제 종료해서 사용자 작업 흐름이 끊기지 않게 해야합니다
이러한 모바일 환경 조건을 고려해볼때 App Component(앱 구성요소)는 개별적이고 비순차적으로 실행될 수 있으며 Android OS나 사용자가 언제든지 앱 구성요소를 제거할 수 있다는 것을 알 수 있습니다
하지만 개발자는 이러한 이벤트(앱 구성요소가 개별적, 비순차적으로 실행 / OS에 의한 앱 구성요소 제거)를 직접 제어할 수 없습니다😂
따라서 이러한 이벤트가 발생할 것에 대비해 앱 구성요소에 앱 데이터나 상태를 저장해서는 안되며 앱 구성요소가 서로 종속되도록 개발해선 안됩니다
앱 구성요소에 앱 데이터나 상태를 저장할 경우 앱의 강제종료나 예상치 못한 전화, 알림 등이 왔을 경우 앱 구성요소가 저장하고 있는 데이터는 유실될 수 있기 때문입니다
또한 앱 구성요소가 서로 종속된다면 포그라운드에서 동작하는 Activity를 종료했을 경우 백그라운드에서 동작하는 Service도 함께 종료되므로 앱 구성요소는 서로 독립적으로 작동해야합니다
위 내용을 통해 '앱 구성요소에는 앱 데이터와 상태를 저장해선 안된다'라는 걸 알 수 있었습니다
그렇다면 앱 데이터와 상태는 어디에 저장해야 앱을 잘 설계(클린 아키텍쳐 구현)했다고 소문이 날까요?🤔
일단 앱을 설계하는데 필수적인 원칙들을 살펴 본후 위 질문에 대한 답을 알아봅시다
이 원칙은 안드로이드 앱 아키텍쳐 원칙 중 가장 중요한 원칙입니다.
안드로이드 초기 개발자들은 Activity나 Fragment에 모든 코드를 작성하는 '실수'를 범하기도 했습니다
Activity나 Fragment 클래스를 UI 기반 클래스 라고 하는데 원칙적으로 UI 기반 클래스는 UI를 핸들링하거나 안드로이드 OS와 앱의 상호작용을 처리하는 로직만 포함해야 합니다
UI 기반 클래스를 최대한 가볍게 유지해야 많은 수명 주기 관련 문제를 피할 수 있을 것입니다
Activity와 Fragment 클래스는 Android 운영체제와 Application(앱) 사이의 계약을 나타내도록 이어주는 클래스일 뿐입니다.
즉, 여러 수명 주기를 제어함으로써 사용자가 어떤 동작을 하더라도 그 흐름이 끊어지지 않도록 해주는 것이 UI 기반 클래스 라고 생각하면 됩니다
따라서 UI 기반 클래스에서는 수명 주기에 따라 발생할 수 있는 상황에 대처하는 코드만 작성하는 것이 좋습니다
안드로이드 앱 아키텍처 원칙 중 두 번째로 중요한 원칙은 모델과 UI를 분리해야 한다는 것입니다
이 말의 의미는 UI 기반 클래스에 데이터나 상태를 저장하지 말라 는 말과 같습니다
모델은 앱의 데이터를 담당하는 구성요소 로, 앱의 View 객체 및 앱 구성요소와 독립되어 있는 존재여야 합니다
즉, 모델을 UI와 분리하여 앱의 수명 주기나 여러 사용자 환경 흐름에 영향을 받지 않도록 해야한다는 것입니다
UI Layer
: 화면에 애플리케이션 데이터를 표시하는 UI 레이어Data Layer
: 앱의 비즈니스 로직을 포함하고 애플리케이션 데이터를 노출Domain Layer
: UI와 데이터 레이어 간의 상호작용을 간소화하고 재사용하기 위함본격적으로 Android Developer 도큐먼트-UI Layer부터 분석해보겠습니다
해당 문서에 나오는 용어들을 살짝 정리하고 가겠습니다
UI
: UI element(버튼) + UI State를 결합한 것 ex)로그인 버튼 활성화 되어있는 화면
UI 컨트롤러(Fragment/Activity)
: UI에 View를 그리거나 사용자 event를 trigger하는 등 UI관련 동작을 캡쳐함
UI 데이터
: 사용자 눈에 보여야 하는 데이터
stateHolders(ViewModel)
: UI State를 생성하는 역할을 하며 생성 작업에 필요한 로직을 포함하는 클래스
UI element
: 버튼같은 컴포넌트
(사진 1)
(사진 2)
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
//데이터를 가공하는 비즈니스 로직이 ViewModel에 있는게 아니었구나!
suspend fun modifyData(example: Example) { ... }
}
Data Layer는 여러개의 DataSource로 추상화되어있는 Repository로 구성됩니다. 앱에서 처리하는 다양한 유형의 데이터 별로 ~Repository라는 이름의 클래스를 만들어야 합니다
Repository 클래스에서 담당하는 작업은 아래와 같습니다
Domain Layer나 UI Layer는 데이터 소스에 직접 액세스 하면 안됩니다. 데이터 영역의 진입점은 항상 Repository클래스여야 합니다. UI Layer의 StateHolder(ViewModel) 같은 곳에 데이터 소스가 직접 종속 항목으로 있어서는 안됩니다(data class 등으로 만든 model 객체 생성X)
Repository 클래스를 데이터의 진입점으로 사용하면 아키텍쳐의 다양한 레이어를 독립적으로 확장할 수 이씁니다.
Data Layer의 Repository는 Repository클래스가 담당하는 데이터의 이름+Repository
라고 이름지어야 합니다
위 사진처럼 Repository클래스(저장소)에 더 복잡한 비즈니스 요구사항이 포함된 경우엔 저장소가 다른 저장소에 종속될 수 있습니다. 관련된 데이터가 여러 데이터 소스의 집계이거나 책임이 다른 저장소 클래스에 캡슐화되어야 하기 때문입니다.
비즈니스 모델
데이터 영역에서 노출하려는 데이터 모델은 다양한 데이터 소스에서 가져오는 정보의 하위 집합일 수 있습니다. 네트워크 및 로컬의 다양한 데이터 소스가 애플리케이션에 필요한 정보만 반환하는 것이 좋으나 실제 이런 경우는 많지 않습니다.
예를 들어 기사 정보뿐만 아니라 수정 기록, 사용자 댓글, 일부 메타데이터도 반환하는 News API 서버가 있다고 합시다.
data class ArticleApiModel(
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
)
data class Article(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val authorName: String,
val readTimeMin: Int
)
class FormatDateUseCase(userRepository: UserRepository) {
//복잡한 비즈니스 로직 처리
private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)
operator fun invoke(date: Date): String {
return formatter.format(date)
}
}
현재 시제의 동사 + 명사/대상(선택사항) + UseCase
Repository클래스가 담당하는 데이터의 이름+Repository
이었습니다!)class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }