저번 글에서는 클린 아키텍처에 대해서 다시 작성했습니다. 이번에는 그 클린 아키텍처와는 조금 다른 Android App Architecture Guide에서 제공하는 아키텍처에 대해서 알아볼 예정입니다. 하지만 이 아키텍처의 가이드 역시 공식 문서가 잘 설명되어 있으니 요약해서 작성해보려고 합니다. 목표는 기존에 앱 아키텍처 가이드를 작성하던 사람이 헷갈릴 때 빠르게 볼 수 있는 수준이면 좋을 것 같네요.
Android App Architecture Guide
공식 문서에서는 일반적인 아키텍처의 원칙에 대해 먼저 이야기합니다. 여기서 나오는 부분은 관심사 분리와 UI 도출, 그리고 단일 소스 저장소인데요. 관심사 분리의 경우 저번에 Clean Architecture를 정리하면서 이미 한 번 얘기된 내용이라 생략하겠습니다.
Drive UI from data models
Another important principle is that you should drive your UI from data models, preferably persistent models. Data models represent the data of an app. They're independent from the UI elements and other components in your app. This means that they are not tied to the UI and app component lifecycle, but will still be destroyed when the OS decides to remove the app's process from memory.
데이터 모델에서 UI 도출하기
또 하나의 중요한 원칙은 데이터 모델에서 UI를 도출해야 한다는 것입니다. 가급적 지속적인 모델을 권장합니다. 데이터 모델은 앱의 데이터를 나타내며, 앱의 UI 요소 및 기타 구성요소로부터 독립되어 있습니다. 즉, 이들은 UI 및 앱 구성요소 수명 주기와는 관련이 없습니다. 하지만 OS가 메모리에서 앱의 프로세스를 삭제하기로 결정하면 데이터 모델도 삭제됩니다.
하나는 영문판에서, 하나는 한국판에서 발췌했습니다. 처음에는 UI에 관한 이야기를 하길래 UiState에 대한 이야기인가? 라는 생각을 잠깐 해봤었는데, 데이터 모델이라는 이름도 그렇고 UI와 독립적이라는 부분도 보고 나니 저번 클린 아키텍처에서 봤던 Data Layer의 data model 및 Domain Layer의 Entity 등과 같은 Data Class를 의미한다고 추측하게 되었습니다.(아니면 댓글 부탁드립니다. 하하...)
또한 지속적인 모델을 가져가라는 의미는 아래와 같은 설명을 볼 때, 작게는 ViewModel이나 object class(singleton 패턴의)등을 이용해 앱이 잠시 백그라운드에 머무는 동안에도 데이터는 유지되어 다시 돌아올 때 UI는 기존과 동일한 상태처럼 보여야 한다는 것을 의미하며, 크게는 서버와의 통신을 통해 불러오거나 사용자를 통해 입력받은 데이터 중 UI 구성을 위해 지속적으로 필요한 데이터를 앱 내에 저장하라는 의미로 받아들여집니다.
이는 카카오톡이 채팅을 로컬DB에 저장해두어 네트워크 연결이 끊겼을 때 기존에 받았던 채팅은 문제 없이 볼 수 있도록 처리하는 것이나, 오랫동안 앱에 백그라운드에 두어 OS가 앱의 프로세스를 강제 종료시킨 후에도 데이터를 가지고 있는 형태(예를 들어 accessToken과 같은)를 의미하는 것 같습니다.
- Android OS에서 리소스를 확보하기 위해 앱을 제거해도 사용자 데이터가 삭제되지 않습니다.
- 네트워크 연결이 취약하거나 연결되어 있지 않아도 앱이 계속 작동합니다.
SSOT(Single-Source Of Truth)
앱에서 새로운 데이터 유형을 정의할 때는 데이터 유형에 단일 소스 저장소(SSOT)를 할당해야 합니다. SSOT는 데이터의 소유자이며, SSOT만 데이터를 수정하거나 변경할 수 있습니다. SSOT는 이를 위해 불변 유형을 사용하여 데이터를 노출하며, 다른 유형이 호출할 수 있는 이벤트를 수신하거나 함수를 노출하여 데이터를 수정합니다.
단일 소스 저장소 라고 번역되어 있는 SSOT는, 말 그대로 데이터 처리를 일원화하는 것을 의미합니다. 특정 데이터는 특정 저장소에서만 처리한다는 것인데요.
- 특정 유형 데이터의 모든 변경사항을 한곳으로 일원화합니다.
- 다른 유형이 조작할 수 없도록 데이터를 보호합니다.
- 데이터 변경사항을 더 쉽게 추적할 수 있도록 합니다. 따라서 버그를 발견하기가 쉬워집니다.
이 패턴의 이점으로 나열된 부분들을 확인해보면 SSOT는 Clean Architecture의 DataSource부분을 의미하는 것 같습니다. 애초에 다른 아키텍처니 정확하게 매칭되지는 않겠으나, 서로가 어느정도 유사점을 보이는 관계를 생각해보았을 때 특정 데이터의 유형(유저에 대한 정보 등)이라면 특정 Class(유저 정보를 다루는 DataSource, Repository등)를 통해 get, set 되어야 한다는 것을 의미합니다.
하지만 이 부분은 어떤 파트에 적용하느냐에 따라 조금씩 다른데요. DBA가 사용할 땐 DB의 정책을, Android의 경우 위와 같이, 또 Swift나 백엔드로 넘어가면 조금씩 다르게 적용되는 것 같습니다. 하지만 안드로이드에서는 저정도의 의미로 사용하는 것 같으니, 필요하다면 다른 파트 부분에서의 아키텍처를 공부해보면 좋을 것 같습니다.
Layer
안드로이드 앱 아키텍처의 계층에 대한 설명을 해보겠습니다. 공식 문서에서 제공하는 계층은 기본적으로 Clean Architecture와 비슷하게 UI Layer(Presentation Layer) - Domain Layer - Data Layer로 이어지는 것은 동일하지만 궁극적인 차이점이 있습니다. Domain Layer가 optional로 표시된다는 점과 종속성의 방향입니다. Android App Architecture의 경우 종속성이 한 방향으로만 움직입니다.
- 반응형 및 계층형 아키텍처
- 앱의 모든 레이어에서의 단방향 데이터 흐름(UDF)
- 상태 홀더가 있는 UI 레이어로 UI의 복잡성 관리
- 코루틴 및 흐름
- 종속 항목 삽입 권장사항
문서에서는 위와 같은 형태를 권장하고 있다고 합니다. 계층형의 경우 애초에 가이드 자체가 계층형 아키텍처를 기준으로 작성되었기 때문에 당연하다고 볼 수 있는 형태입니다. 위에서 권장하는 형태로 작업을 진행하게 되면, 각 레이어는 Flow(반응형을 위한, RxJava로도 가능)를 통해 각 계층별 통신을 진행하고 UDF를 사용한다면, UI에서 발생한 Event는 ViewModel을 거쳐 DomainLayer에 전달되고, 거기서 또 다른 로직을 거쳐 Data Layer어에 도달하게 될 겁니다. 그렇게 도달한 이벤트를 통해 특정 비즈니스 로직을 실행시키고 난 결과물은 UI Layer에서 collect하고 있는 데이터를 통해 다시 UI Layer에 도달하게 될 것 입니다.
이를 간단하게 표현하자면
- 버튼의 onClick 호출
- onClick에서 viewModel.handleEvent(event) 호출
- viewModel의 handleEvent 메소드에서 UseCase 호출
- UseCase에서 Repository(혹은 DataSource)를 이용해 Entity 가져와서 정의된 형태로 반환(Flow 혹은 primitive, 아니면 미리 정의된 Response 타입)
- 파라미터를 이용해 ViewModel에서 비즈니스 로직 정리
- UI에게 넘겨줄 action data 수정
- UI(Fragment, Compose Component, Activity 등)에서 collect 중인 viewModel의 action을 감지해 UI 수정(DataBinding 또는 recomposition 등) 및 화면 이동 등의 기능을 수행(navigation을 이용한 move 또는 startActivity 등)
위와 같은 흐름으로 진행되게 됩니다. 이때 자연스럽게 단방향 데이터 흐름을 위해 코루틴 및 Flow를 사용하게 됩니다. 또한 Hilt와 같은 Di 모듈을 이용해 각 레이어를 연결해주는 파라미터를 받아오게 되면 좀 더 유기적으로 코드를 작성할 수 있게 됩니다.
위와 같은 권장사항들은 안드로이드 앱 아키텍처 가이드의 기본 근간이 되는 계층형 및 반응형 아키텍처를 제외하면 꼭 지켜야할 필수사항은 아니지만, 대체적으로 각 권장사항 별로 그렇게 사용하는 이유와 장점이 명확합니다. UDF나 DI, 코루틴 및 StateHolder 등은 모두 각각의 장단점이 있고, 이들을 공부한 후 적용했을 때 얻을 수 있는 장점이 크다면 적용하면 되겠습니다.
UiLayer는 Clean Architecture의 Presentation Layer와 동일한 개념으로 볼 수 있습니다. 일단 View를 표시하기 위한 Fragment, Activity, Component등에 UI를 표시하기 위한 데이터를 연결합니다. 이때 UI에 표시하기 위해 사용되는 데이터는 다른 계층에서 전달받은 데이터 중 일부에 해당하며 가공한 데이터를 UiState로 정의하고, 이를 표시하는 Component들을 UiElements로 정의합니다.
이 UI Layer에 가장 중요한 점은 크게 2가지입니다. 하나는 생명주기이며, 다른 하나는 View는 UiState로 대표되는 데이터 홀더들이 전부 읽기전용으로 지정되어야 한다는 것입니다.
기본적으로 안드로이드의 UI는 생명주기를 가지고 있습니다. 이에 대한 부분이 Compose와 XML이 조금 다르긴 하지만, 어쨌든 이 생명주기에 맞춰 UiState를 collect 해야 합니다. 이는 공식문서에 작성되어 있는 예제 소스에서 코드를 어떤 방식으로 작업하는 지 확인할 수 있습니다. 이러한 생명주기가 중요한 이유는 메모리릭은 당연한 부분 중 하나이고, 만약 이 생명주기가 지켜지지 않는 상태에서 collect가 계속 이루어져 데이터의 변화에 따라 UI를 변경시키려고 할 것이고, 이때 UI를 참조하려는 코드가 NPE를 발생시키는 휴먼 에러를 경험할 수도 있게 됩니다.
읽기전용의 경우, ViewModel 밖에서는 set할 수 없도록 val 혹은 Not Mutable로 처리하라는 것을 의미하는데요! 이는 ViewModel 이외의 Class(예를 들어 View)에서 ViewModel의 UiState를 수정하게 되는 경우 아키텍처의 여러가지 사항들이 위배될 뿐 아니라 ViewModel 외부에서 상태가 바뀌게 됨으로서 상태 추적이 어려워지기 때문에 항상 닫아둬야 합니다. 물론 이는 MVVM 패턴을 사용하는 경우 필수적인 요소 중 하나이기도 하며, 단방향 데이터 흐름을 사용하는 이상 필수적으로 요구되는 부분입니다.(그리고 아키텍처에 맞게 코드를 짜려고 노력하다보면 자연스럽게 이렇게 작성하는 부분이 훨씬 코딩이 쉬워짐을 느끼게 됩니다...)
도메인 레이어는 복잡한 비즈니스 로직이나 여러 ViewModel에서 재사용되는 간단한 비즈니스 로직의 캡슐화를 담당합니다. 모든 앱에 이러한 요구사항이 있는 것은 아니므로 이 레이어는 선택사항입니다. 따라서 복잡성을 처리하거나 재사용성을 선호하는 등 필요한 경우에만 도메인 레이어를 사용해야 합니다.
클린 아키텍처에서는 프로젝트의 크기가 충분히 작은 경우(혹은 서버에서 내려주는 데이터의 가공 및 로직 처리가 거의 없는 경우) 대부분 UseCase는 Repository의 메소드를 그대로 호출해서 전달해주거나, Repository의 Response 값을 다시 UseCase의 Response에 맞게 바꿔주는 역할이 전부인 경우가 많습니다. 그래서인지 공식문서에서 아예 위와 같은 문장을 남겨뒀더라구요.
공식문서에서 Domain Layer는 간단합니다. UseCase 외에는 크게 정해주는 게 없거든요. 사실상 UseCase를 제외하고 필요한 부분은 각 프로젝트의 영향도에 따라 알맞게 설계해서 사용하라는 느낌 같습니다.
안드로이드 앱 아키텍처는 기본적으로 Domain Layer가 없을 수 있기 때문에, Ui Layer가 Data Layer를 직접 참조할 수 있는 구조로도 작성이 가능합니다. 그러나 중간 역할을 하는 Domain Layer가 작성되었을 때 Ui Layer에서 Data Layer에 대한 접근을 제한할 지 말지 여부가 중요한데, 공식문서에서는 모든 사항에 대해 제한하는 경우 불필요한 UseCase가 추가되어 복잡도가 증가하는 문제가 발생할 수 있으니 부분적으로 제한해두는 것을 권장하고 있네요.
의존성 방향이 항상 한 쪽으로 향하고 있기 때문에 굳이 Mapper가 필요하지는 않습니다(더욱이 관련 내용도 공식문서에 없습니다). Data Layer에서 사용되는 모든 클래스를 Ui Layer와 Domain Layer에서 쓸 수 있기 때문이죠. 물론 만들 수도 있고, 매핑을 위한 확장함수등을 구현할 수도 있습니다.
공식문서에서 말하는 이 계층의 큰 특징이 몇가지 있는데요. DataSource Class에는 오직 Repository의 이름을 달고 있는 Repository pattern의 Class만 접근할 수 있다는 것입니다. 아래와 같은 문구로 설명하고 있습니다.
계층 구조의 다른 레이어는 데이터 소스에 직접 액세스해서는 안 됩니다. 데이터 레이어의 진입점은 항상 저장소 클래스여야 합니다. 상태 홀더 클래스(UI 레이어 가이드 참고) 또는 사용 사례 클래스(도메인 레이어 가이드 참고)의 경우 데이터 소스가 직접 종속 항목으로 있어서는 안 됩니다. 저장소 클래스를 진입점으로 사용하면 아키텍처의 다양한 레이어를 독립적으로 확장할 수 있습니다.
또 다른 점은 Data Layer의 DataModel은 타 계층에서 수정이 불가능해야 합니다. Data Layer에 데이터를 호출한 Ui Layer의 특정 Class가 이 데이터를 가공할 때 UiState를 사용해 자신이 사용할 수 있는 Class로 변경해야 하는 것이지, Data Layer에서 가져온 DataModel을 직접 변경하게 되면 안된다는 것을 의미합니다. 그렇지 않으면 동일한 Data를 가져왔음에도 추후 A와 B Class에서 사용하는 Data가 달라질 수 있게 됩니다.
이외에도 공식가이드에서는 repository 및 datasource에 대한 가이드를 알려주고 있습니다. 저는 Retrofit을 주로 이용하기 때문에 DataSource 작업을 할 때에는 retrofit api interface의 반환값을 그대로 돌려주거나 api 그 자체를 dataSource로 사용하기 위해 하나의 api에 연관된 여러 메소드들을 작업해두곤 합니다. Room을 사용하는 경우에도 dao가 dataSource 그 자체가 되기도 하구요. 이 부분에 대해서는 사람들마다 의견이 다 다른 것 같습니다만, 안드로이드 앱 아키텍처 가이드에서 가장 중요시하는 것은 불필요한 코드를 사용하지 않는 것이니, 누가 틀리고 누가 맞다고 쉽게 말하기는 어려울 것 같습니다.
안드로이드 아키텍처 가이드는 확실히 친절하다는 생각이 듭니다. 기본적으로 네이밍이나 여러가지 예제들을 잘 제공해줘서 따라하기 편하게 되어 있습니다. 물론 조금이라도 프로젝트 규모가 생기게 되면 예제만으로는 충분하지 못한 경우가 많이 생기지만, 그 부분은 당연히 겪어야 할 부분이라 감안한다면 굉장히 상세하고 자세한 예제 가이드가 아닌가 싶습니다(그래서 초기에 학습할 때 안드로이드 개발자들의 코드가 점점 더 비슷해지는 걸까요...).
클린 아키텍처에 대한 이야기 빼고, 차이점도 빼고 다루려니 생각보다 다룰게 없었습니다. 공식 가이드에 잘 나와있는 부분을 요약만 하면 되는 거다 보니... 옛날에는 안드로이드의 권장 아키텍처가 곧 클린 아키텍처가 아닌가 라는 생각을 많이 했습니다. 겉만 살짝 보면 계층 별로 나뉘어져 있는 것도, 각 계층들이 하는 역할도 비슷하기 때문인데요. 어느 순간 두 개를 같은 선상에 놓고 코딩을 하다 보니 많이 다르다는 것을 알겠더라구요. 지금 생각해보면 가이드와 클린 아키텍처를 열심히 읽어보기만 해도 많은 부분이 다르다는 것을 알았을 텐데, 그 때는 참 좁은 세상에 살고 있지 않았나 싶습니다.
참고
https://developer.android.com/topic/architecture?hl=ko
https://developer.android.com/topic/architecture/recommendations?hl=ko
'안드로이드 > 기타' 카테고리의 다른 글
Android Clean Architecture 와 Android App Architecture - 3 (1) | 2024.07.26 |
---|---|
Android Clean Architecture 와 Android App Architecture - 1 (1) | 2024.06.09 |
안드로이드 버전별 점유율 2024.11(업데이트) (3) | 2024.02.25 |
TargetSDK 33 버전으로 업데이트 (0) | 2023.08.30 |
SAA(Single Activity Architecture) - with jetpack Navigation (1) | 2023.02.23 |
댓글