잠이 안오는 밤에 생각을 정리하기 위해 글을 작성 하게 되었습니다.
안드로이드 설계 관한 이야기를 하기전에, 안드로이드 개발을 이야기 해보자면 안드로이드 개발은 Activity, Service, ContentProvider, BroadCastReceiver 크게 4개의 컴포넌트가 존재합니다.
이 네가지는 공통적으로 Context를 제공해주며 앱에 진입할 수 있는 진입점이 됩니다.
또한 안드로이드에서 어플리케이션은 기본적으로 반드시 1개 이상의 Activity가 필요합니다. Activity가 없는 어플리케이션은 실행 할 수 가 없습니다. 그로므로 manifest에 최소 1개의 Activity 가 있어야 하며 앱 실행의 시작점을 Activity intent-filter안에서 명시 해놔야 합니다.
Activity는 사용자와 상호 작용할 수 있는 인터페이스를 제공합니다. 사용자와 직접적으로 상호작용 하기 때문에 UI를 보여주거나 유저 입력을 받아 처리하기 합니다. 그렇기 때문에 처음으로 안드로이드 개발 할때 기본적으로 Activity를 자주 접하게 되고 Activity를 중심으로 개발을 하게 됩니다. 그래서 개발을 하다보면 Activity에 많은 로직들을 포함하게 되는 데 아래와 같은 문제가 발생합니다.
Activity 로직 몰빵
별도의 안드로이드 설계를 생각하지 않는다면 UI를 동작하기 위한 로직과 Model, View 사이에 상호작용 하는 로직이 Activity 한곳에서 진행됩니다.
(1) XML이 정적으로 선언되어 뼈대가 되는 UI의 모습이 구성 되지만 런타임에서 XML을 통해 UI를 변경할 수 없습니다.
(2) 위 이유로 모델에서 제공 받은 데이터를 UI에 맞게 데이터를 재구성하고 재구성된 데이터로 UI를 그리는 일까지 Activity가 처리하게 됩니다.
(3) Activity를 통해 사용자 입력을 받고 입력 반응에 필요한 데이터를 모델에서 찾습니다. 그 다음 사용자 요구에 맞는 View(xml inflate,혹은 Android View Class)를 골라서 데이터와 조합한 후 View를 보여주는일을 Activity가 하게 됩니다. (mvc의 컨트롤러와 완전히 같지는 않지만 비슷한 모습을 가지게 됩니다. 심지어 View의 역활을 Activity가 함께 하기도 합니다.)
위와 같은 이유로 Activity는 매우 비대해지는데 이는 곧 안드로이드 앱 환경에서 큰 문제가 됩니다.
앱 컴포넌트는 기존에 실행중인 것이 없다면 처음 실행시에 Android OS의 쓰레드로 Linux 프로세스를 생성합니다.
반면, 기존에 실행중 컴포넌트가 있으면 새로 생성하지 않고 기존에 사용중인 프로세스를 사용합니다.
Android OS의 쓰레드로 실행하는 것이기 때문에 Android OS 관리하에 기기의 메모리 부족시 앱의 프로세스를 kill 시킬 수 있습니다. Activity의 경우 Process 가 kill이 되면 보유중인 데이터가 사라집니다. (별도의 saveInstance 처리를 하지 않았다면) Activity로 돌아온다면 다시 onCreate 부터 라이프사이클이 시작되게 됩니다.
뿐만 아니라, 실행중인 앱은 여러 컴포넌트가 비순차적으로 실행 될 수 있고 (Activity와 함께 Service, BroadCastReceiver 동작중 등등.. ) 사용자가 다른앱과 상호작용을 하는 경우가 많고 (Android OS가 여러앱을 통해 메모리가 부족하게 되면 내 어플의 Process를 종료) 사용자가 강제적으로 프로세스를 종료할 수도 있습니다.
그렇기 때문에 프로세스가 언제든지 종료 되도 이상하지 않으므로 컴포넌트에 앱 데이터나 상태를 저장하는 것은 좋지 않습니다.
앱 컴포넌트로부터 독립된 설계
Activity는 화면 UI를 가지기 때문에 앱이 소유한 화면처럼 느껴질수는 있으나, 사실상 Activity는 Android OS와 어플리케이션 간의 상호작용 하는 클래스입니다. 그래서 Activity는 UI 기반 로직 및 OS 와 상호작용을 우선적으로 생각해서 사용해야 합니다.
그렇다면 UI기반 로직과 OS 상호작용을 Activity에서 분리한다면 다른곳에서 처리해야할 것이 몇가지가 있습니다.
1. 뷰의 상태를 보관하는 역활
2. 앱 데이터를 UI를 표시 하기위한 UI 상태로 재구성 하는 로직을 담당
3. 앱 데이터를 제공 및 가공해주는 Model(data, domain Layer) 에 의존
Activity가 관심을 가질 필요가 없는 위 역활들은 다른 클래스로 책임을 위임 해야합니다.
그리고 위와 같이 클래스의 관심을 분리하는 것을 안드로이드 권장 설계에서 가장 중요한 최우선 원칙으로 생각합니다.
이를 관심사 분리라고 말합니다.
AAC ViewModel
https://developer.android.com/topic/libraries/architecture/viewmodel
안드로이드는 공식적으로 Activity로 부터 분리된 책임을 맡아주기에 최적의 환경을 자랑하는 AAC ViewModel이 제공됩니다.
AAC ViewModel은 데이터와 상태를 특정 View의 Lifecycle에 보다 더 오래 유지합니다. Destory보다 더 오래 데이터와 상태를 유지하기 때문에 configration change 에 있어서 안정성이 있으며 View LifeCycle에 아무런 영향이 받지 않으면서 데이터와 상태를 보존 할수 있습니다.
StateHolder
StateHolder은 Model(data, domain Layer)에서 앱 데이터를 받아오거나 상호작용 합니다.
앱 데이터를 가지고 UI를 표시 하기 위한 UI상태로 변환 합니다.
그래서 StateHolder는 Model(data, domain Layer)에 의존 하게 됩니다.
StateHolder는 UI상태를 관리하기 때문에 StateHolder에서 발생하는 이벤트 결과는
기본적으로 UI상태 업데이트가 이루어집니다.
그렇다면 분리했던 아래 3가지를 StateHolder (ViewModel)가 모두 책임을 맡아 처리하면 딱 알맞는 모습이 됩니다.
1. 뷰의 상태를 보관하는 역활
2. UI 상태에 맞게 앱 데이터를 재구성 하는 로직 담당
3. 앱 데이터를 제공 및 가공해주는 Model(data, domain Layer) 에 의존
그리고 안드로이드 공식 설계는 위에서 설명한 AAC ViewModel을
StateHolder 개념으로 사용하기를 권장합니다.
그로므로, Activity는 UI, ViewModel은 StateHolder로 관심을 분리 합니다.
하지만 주의할점이 있습니다.
- AAC ViewModel은 Owner로 설정된 View 보다 더 오래 유지되기 때문에 View Type의 context 를 AAC ViewModel에 가지고 있으면 Activity가 소멸 되어도 ViewModel에서 참조가 유지 되고 있기 때문에 메모리 누수가 일어날 수 있습니다.
그래서 UI로직이 View Type의 context 를 필요로 하는가에 따라
UI 로직이 UI와 StateHolder중에 어디에 있을지는 상황에 따라 달라집니다.
사용자 입력은 어디서 받지?
사용자 입력은 여전히 Activity(UI)에서 받습니다. 주시할점은 Activity를 바라보는 시선이 이전과 달라야합니다.
Activity를 UI로 바라보고 이 UI는 사용자로 부터 입력을 받아야 한다는 개념으로 생각해야합니다.
Activity는 사용자 입력을 받고 UI Element 와 관련된 휘발성 데이터(ex: Animation, RecyclerView Adapter 등등.. )와 같이 특별한 경우가 아니라면 UI 상태를 들고 있는 ViewModel에 이벤트를 넘깁니다.
Activity는 오로지 UI 기반 로직과 OS상호작용만 생각해야 합니다.
사용자 입력에 따른 결과는 어떻게 처리하지?
ViewModel은 다시 Activity 에게 결과를 알립니다. 여기서 결과를 알리는 방식이 중요한데, 아래에서 상세히 설명하겠습니다.
단방향 데이터 흐름
사용자 이벤트를 UI(ex:Activity) 가 받고 StateHolder(ex:ViewModel) 로 전달하고 Model 쪽으로 이벤트를 전달합니다. 이벤트로 인해 변경된 상태는 반대순서로 이어지며 UI로 전달 됩니다.
상태 변경이 필요한 이벤트일 시 이러한 싸이클이 단방향으로 반복 됩니다.
Event: 저수준 -> 고수준의 계층순으로 이벤트가 이동해서 이벤트가 위로 흐른다라고 이야기 합니다.
State: 위에서 처리한 이벤트에 의해 업데이트 된 State을 다시 StateHolder에게 전달하고 UI상태에 맞게 데이터를 재구성 한 뒤 UI로 전달합니다. 상태가 위에서 아래로 흐른다고 합니다.
그래서 하나의 흐름을 이루게 됩니다.
UI -> StateHolder -> UI 혹은
UI -> StateHolder -> Model(data,domain Layer) -> StateHolder -> UI
위 같이 이벤트는 위로 흐르고 상태는 아래로 흐르는 것을 단방향 데이터 흐름 이라고 합니다.
그리고 이 흐름을 하나의 데이터 스트림으로 볼 수 있습니다.
사용자 입력의 결과는 어떻게 받지? 에 대한 상세한 답은 아래와 같습니다.
- 단방향 흐름에 따라서 데이터 스트림에 의해 UI상태가 변화시 UI도 변화 해야 합니다. 그래서 UI 상태에 따라 UI를 변화 시키기 위해 Observable data holder를 사용하게 됩니다. UI상태를 Observeable data Holder 타입으로 저장한다면 UI상태 변화시 UI가 자동 업데이트 하게 됩니다.
안드로이드 개발에서 자주쓰이는 Observeable data Hodler 는 대표적으로 Flow 와 LiveData가 있습니다. 안드로이드 개발자는 Observeable data Holder 를 지원하는 여러 방식 중에 각각 장단점을 파악하여 상황에 맞는 것을 사용하면 됩니다.
Flow
https://developer.android.com/kotlin/flow
Live Data
https://developer.android.com/topic/libraries/architecture/livedata
단방향 흐름의 장점
단방향 흐름을 사용함으로써 상태가 위에서 아래 방향으로 흐르기 때문에 다른 액션으로 인해 View의 State이 꼬일일이 줄어들며,
State이 변경되는 위치, 변환되는 위치, 사용되는 위치가 확실히 구분 됨으로 좋은 유지보수 환경이나 좋은 테스트 가능성을 만들어 낼 수 있습니다.
Layered Architeture
계층을 확실히 구분하는 아키텍처 입니다. 안드로이드 권장 설계는 Layered Architeture와 흡사한 형태를 가지고 있습니다. 안드로이드에서 공식적으로 설명하는 아키텍처 가이드는 다음과 같습니다.
안드로이드 권장 설계 안내
https://developer.android.com/jetpack/guide?hl=en
UI Layer (UI와 StateHolder)
위에서 설명한 UI와 StateHolder 포함 합니다.
UI 와 State Holder 는 함께 UI 기반 로직을 수행합니다. 이 둘은 UI element + UI State 을 조합 하여 사용자에게 보여줄 UI를 보여줍니다. 이 둘을 묶거나 UI기반 관련된 로직을 수행하는 계층을 Presentataion Layer 혹은 UI Layer라고 부릅니다. UI Layer는 앱 데이터를 UI로 표시할 수 있는 형식으로 바꿔서 최종적으로 사용자에게 시각적으로 데이터를 보여주는 역활을 합니다.
Data Layer
Data Layer 는 앱 데이터에 대한 비즈니스 로직을 처리합니다. 안드로이드 설계에서 권장하는 Data Layer는 여러 출처의 데이터 소스를 한곳에서 관리하는 Repository를 가지고 있고 Repositoty는 외부 계층에서 접근할 수 있는 유일한 방법 입니다. 외부 계층은 Repository를 통해 Data Layer 에 접근 해서 데이터 소스를 가져오거나 데이터 소스의 업데이트를 요청 합니다.
StateHolder(ex:ViewModel) 가 직접 DataLayer 에 Repository 접근 해서 앱 데이터를 사용하게 할 수 있습니다. 하지만 하나의 StateHolder에 DataLayer의 여러 Repository를 다루다 보면 StateHolder 가 가지는 책임이 매우 커지게 됩니다.
그래서 안드로이드 공식 설계는 하나의 계층을 하나 더 추가 했습니다.
Domain Layer
Domain Layer 는 여러 비즈니스 로직을 캡슐화 합니다. 단일 원칙 책임을 따르는 UseCase라는 명칭으로 하나의 비즈니스 로직을 하나의 클래스로 관리 합니다. 여러 Repository 를 통해 하나의 로직을 만들어낼 수 있으며 이미 만들어낸 UseCase와 다른 Repository를 결합하여 하나의 로직을 만드는 등등.. 재사용성을 가진 클래스를 만들어 낼 수 있으며, UseCase는 하나의 책임만 맡으므로 책임이 큰 클래스가 생기지 않습니다. 또한 StateHolder는 재사용성을 위해 만들어진 UseCase를 사용함으로써 StateHolder에 보일러 플레이트 코드가 많이 사라집니다.
안드로이드 공식 설계는 Domain Layer를 필수 계층으로 지정하지 않습니다. 비즈니스 로직의 재사용 필요성 및 StateHolder의 부담에 따라 필요하면 만들어 사용하라고 권장합니다.
UseCase는 변경할 수 있는 이유가 하나여야 한다는 원칙에 따라서 만들어지는 Class입니다. 그로므로, UseCase 마다 하나의 비즈니스 로직만 가지는데 즉, 비즈니스 로직 갯수 만큼 많은 클래스를 생성해야 한다는 뜻입니다. 앱 제작에 있어서 비즈니스 로직의 재사용의 필요성을 느끼지 못했다면 UI Layer 와 Data Layer 두가지로 충분할 것 입니다.
Android Recommend Architeture
ui -> domain -> data 순으로 의존 흐름을 확인 할 수 있고 각각 계층에 따라 관심사 분리를 준수했고 의존의 끝에 Data Layer가 있습니다. 자세한 내용은 아래에서 더 언급 해보겠습니다.
Layered Architeture
각각 계층을 나누고 의존 방향은 영속적인 것에 향하고 있는 모습을 보면
안드로이드에서 공식으로 권장하는 설계는 Layered Architeture 라고 볼 수 있습니다.
*이 부분부터는 제 주관적인 생각입니다.
왜 안드로이드 권장 설계는 Layered 형태를 가지게 되었는가?
hexagonal Architecture, onion Architecture, clean Architecture의 경우
Domain Driven Development 로 의존성의 끝은 Domain 으로 향하면서 Domain 을 중심으로 구성됩니다.
위 아키텍처들은 domain이 정책을 담당하므로 domain이 상당히 중요한 레이어가 됩니다.
하지만 Layered Architeture 는 DataBase Driven Development 를 따릅니다. 의존 방향은 가장 영구적인 것으로 향하기 때문에 데이터 베이스 기반으로 설계가 됩니다. 그렇기 때문에 Layered Architeture와 같은 형식을 보이는 안드로이드 권장 설계에서 domain보다 data 계층의 중요성을 더 크게 판단하기 때문에 domain Layer는 필수가 아닌 선택이 가능한겁니다.
안드로이드 권장 설계에서 앱 데이터를 앱 컴포넌트에 저장하는 것을 가능하면 피해야한다고 말합니다. 그래서 가장 영속적인 데이터를 기반으로 UI를 그려내는 것을 원칙으로 합니다.
(안드로이드 권장 설계의 가장 이상적인 그림은 네트워크에서 받은 디비를 로컬 디비에 모두 캐싱 처리해서 로컬 디비로 부터 데이터를 사용하는 것을 가장 이상적으로 생각합니다. 하지만 이 작업은 셋팅 하는데 매우 손이 많이가는 것이 단점 입니다.)
영구적인 데이터를 기반으로 UI를 그리는 것이 Android OS 부터 완전히 독립 되어 어떤 상황에서도 데이터의 안전을 보장 할 수 있기 때문에 영구 적인 Database로 부터 데이터를 가져와 사용하는 설계를 우선적으로 생각 했을 것이라 예상해봅니다. 또한, 안드로이드 개발 특성상 clean Architeture 처럼 완벽하게 프레임워크로 부터 독립하려면 타협이 필요할 수도 있고, cleanArchiteture 를 도입 하는 순간 여러 비용이 발생합니다.
반면, Layered Architeture 가 아키텍처는 개발자가 학습 하기 쉬운 비용를 가지고 있으며,
관심사 분리를 충분히 준수하고 있으면서 앱 데이터 기반으로 UI를 도출해야하는 원칙을 상당히 중요한 부분으로 권장하기 때문에
안드로이드 권장 설계는 Layered Architeture의 모습을 선택한 것 같습니다.
MV 시리즈 응용하기
많은 안드로이드 개발자들이 clean Architeture 혹은 Layered Architeture 기반을 두고 Mv시리즈 패턴을 적용하려고 합니다.
이 경우 data ~ domain을 model로 취급하고 ui Layer의 컨트롤러/프레젠터/뷰모델 을 둬서 사용합니다.
특히나 요즘은 mvvm패턴을 많이 사용합니다. mvvm 패턴을 응용한다면 가능한 ViewModel 에 비즈니스로직이 포함 되지 않도록 하면서 프레젠테이션 로직을 위주로 사용해야하고 단방향 흐름의 데이터 스트림 개념보다 ViewModel의 상태의 변화를 View가 관찰해서 View가 업데이트 되고 ViewModel은 아예 View를 몰라야 하는 개념을 더 중점으로 생각하게 됩니다.
하지만 아쉽게도 AAC ViewModel 를 Mvvm의 ViewModel 로써 사용하려 한다면 Mvvm의 ViewModel의 장점을 모두 활용하기는 어렵습니다.
AAC ViewModel 은 View Owner이 무엇인가에 따라 수명주기가 정해집니다. 그래서 특정 View에 대한 종속성이 생김으로 Mvvm
의 특징인 View 와 ViewModel n:1을 활용하기 힘듭니다. 그래서 대부분 1:1로 사용합니다.
n:1로 사용하는 경우는 parentFragmentOwner 또는 상위 Activity Owner를 AAC ViewModel에 지정해놓고 Fragment 간의 데이터 공유로 활용하는 경우가 대부분 입니다.
또한 이미 AAC ViewModel은 View의 Owner를 가지고 있는데 이는 ViewModel은 View를 알고 있다는 이야기가 됩니다. 이러한 타협점을 감안하고 사용하는것이 AAC ViewModel를 사용한 안드로이드의 MVVM입니다.
그래서 만약 완벽한 Mvvm 구현하고 싶다면 AAC ViewModel를 사용하지 않고 별도의 ViewModel Class를 자체적으로 만들어서 AAC ViewModel이 주는 장점을 포기한채 필요한 부분과 불편한점을 커버한 것들을 스스로가 직접 구현해야 합니다.
마무리
- Activity 는 컨트롤러 역활을 기본적으로 수행하면서 뷰의 역활을 동시에 필요로하는 경우가 있기 때문에 설계없이 코딩을 한다면 Activity가 매우 비대해지면서 나중에 유지 보수하기 힘들어집니다.
- 또한 Activity와 같은 앱 컴포넌트 류에 앱 데이터나 상태를 저장하기에는 Android OS 상호작용이나 사용자 강제 종료 등등 위험성이 많으므로 철저하게 관심사 분리를 해야합니다.
- 안드로이드에서 제공하는 AAC ViewModel를 StateHolder로 사용하고, UI Layer 에서 Activity는 ui기반 로직 및 OS 상호작용에만 집중할 수 있도록 하는 것을 권장 합니다.
- AAC ViewModel에 ApplicationContext를 제외한 Context 류의 타입이 존재하면 메모리 누수의 위험이 있습니다. 로직에 Context 필요에 따라 UI 로직의 위치가 바뀌기도 합니다.
- UDF(단방향 흐름)으로 이벤트 반응을 단방향으로 제어합니다. Event 는 올라가고 State은 내려옵니다.
- 안드로이드 권장 설계는 DataBase Driven Devolpment 를 띄는 Layerd Architeture 를 권장 합니다. data 계층에서 제공되는 앱 데이터를 기반으로 UI 계층의 StateHolder에서 UI를 표시 할수 있는 상태로 변환하고 UI에 나타냅니다. 여기서 StateHolder 너무 비대해지는 것을 대비하여 domain Layer를 추가합니다. 관심사 분리를 중요시 여기며 앱 데이터 기반으로 ui 표시를 중요시 합니다.
- 안드로이드 권장 설계에서 Domain은 필수가 아닌 선택입니다. 또한 의존성의 끝은 Domain이 아닌 Data 계층 입니다.
이는 Clean Architeture, Hexagonal architecture, Onion Archteture 등등 과 같은 Domain 기반 설계와 안드로이드 공식 문서에서 설명한 설계와 다름을 이야기 합니다.
- 개발자는 필요에 따라 data , domain을 Model로 취급하고 ui레이어에서 Mv패턴 시리즈를 사용하기도 합니다.
- MVVM ViewModel 과 AAC ViewModel는 이름만 같을 뿐 입니다.
이상 잠 못드는 밤 생각을 정리해본 글이었습니다.
'안드로이드 Android' 카테고리의 다른 글
안드로이드 개발 (37) Kotlin In Action 정리 - 1 (2) | 2023.02.05 |
---|---|
안드로이드 개발 (36) 심플한 Flow Sample (1) | 2022.09.12 |
안드로이드 개발 (34) RecyclerView 성능 향상 (6) | 2022.01.08 |
안드로이드 개발 (33) Coroutine Flow on Android (2) | 2021.12.06 |
안드로이드 개발 시 실수 모음 (1) | 2021.09.20 |