App Architecture Guide
안드로이드 공식문서의 Guide to app architecture를 바탕으로 작성된 포스팅입니다.
Mobile app user experiences
데스크톱 앱은 하나의 모놀리식 프로세스로 진행된다. 반면 전형적인 안드로이드 앱의 구조는 훨씬 복잡한데, Activity, fragment, service, content provider, boradcast receiver 등의 많은 앱 컴포넌트들로 구성된다.
이러한 구성요소들의 대부분은 앱 Manifest에 선언한다. 그러면 Android OS에서 이 Manifest 파일을 통해 기기의 전반적인 사용자 환경에 앱을 통합하는 방법을 결정한다. Android 앱은 여러 구성 요소를 포함하고, 짧은 시간 내에 여러 앱과 상호작용을 할 때도 많기 때문에 앱은 사용자 중심의 다양한 워크플로와 작업에 맞게 조정될 수 있어야 한다.
- 앱에서 앱으로 바꾸는 동작(카메라에서 파일 선택기, 다른 앱 사용 중에 전화)이 일반적이기 때문에 흐름을 올바르게 처리해야 함
- 리소스가 제한되어 있으므로 새로운 앱을 위한 공간 확보를 위해 언제든지 일부 앱 프로세스를 종료해야 할 수 있음
→ 앱 구성요소는 개별적이고 비순차적으로 실행 & 운영체제나 사용자가 언제든지 앱을 제거할 수 있음
→ 이런 이벤트는 직접 제어할 수 없으므로 앱 구성요소에 앱 데이터나 상태를 저장해선 안되며, 앱 구성요소가 서로 종속되면 안됨
Common Architectural principles
⭐ Separation of concerns : 관심사 분리
Activity 또는 Fragment에 모든 코드를 작성해선 안된다.
→ UI 기반의 클래스는 UI와 운영체제 상호작용을 처리하는 로직만 포함해야 한다!
이런 클래스를 최대한 가볍게 유지해야 많은 lifecycle-related problem을 피할 수 있기 때문!
"Keep in mind that you don't own implementations of Activity and Fragment"
Activity와 Fragment의 구현 클래스는 Android OS와 앱 사이를 연결해주는 클래스에 불과하기 때문에 시스템 조건으로 인해 언제든지 OS에 의해 제거될 수 있다. 따라서 수월한 앱 관리를 위해서는 UI 기반 클래스에 대한 의존성을 최소화해야 한다.
⭐ Drive UI from a model : 모델에서 UI 만들기
Model은 앱의 데이터 처리를 담당하는 구성 요소로, 앱의 View 객체 및 앱 구성요소와 독립되어 있어 앱의 수명주기나 관련 문제의 영향을 받지 않는다. 안드로이드 가이드에서는 가급적 지속적인 모델(Persistent Model)을 권장한다.
- Android OS에서 리소스를 확보하기 위해 앱을 제거해도 사용자 데이터가 삭제되지 않음
- 네트워크 연결이 취약하거나 연결되어 있지 않아도 앱이 계속 작동함
데이터 관리 책임이 잘 정의된 Model 클래스를 기반으로 앱을 만들면 테스트가 쉽고 일관성을 유지할 수 있다!
Recommended app architecture
각 구성요소가 한 수준 아래의 구성요소에만 종속된다.
- Activity나 Fragment는 ViewModel에만 종속된다.
- Repository는 여러 개의 다른 클래스에 종속되는 유일한 클래스
Build the user interface : 사용자 인터페이스 제작
UI에 표시되는 데이터 요소에 담길 정보를 유지하기 위해 ACC ViewModel을 사용한다.
- ViewModel 객체 : Fragment나 Activity 같은 특정 UI 구성요소에 관한 데이터를 제공하고 모델과 커뮤니케이션하기 위한 데이터 처리 비즈니스 로직을 포함함
- ViewModel은 데이터를 로드하기 위해 다른 구성요소를 호출하고 사용자 요청을 전달하여 데이터를 수정할 수 있음
- ViewModel은 UI 구성요소에 관해 알지 못하므로 구성 변경(예: 기기 회전 시 Activity 재생성)의 영향을 받지 않음
- LiveData : 식별 가능한 데이터 홀더
- 앱의 다른 구성 요소에서 이 홀더를 사용하여 객체의 변경사항을 모니터링할 수 있음(종속성 경로를 만들지 않고도)
- LiveData 구성요소는 Activity, Fragment, Service와 같은 앱 구성요소의 생명 주기 상태를 고려함
- LiveData 구성요소는 객체 유출(object leaking)과 과도한 메모리 소비를 방지하기 위한 정리 로직을 표함함
데이터 모델 객체에 LiveData를 사용해서 ViewModel에서 LiveData 필드를 적용한 데이터에 대해 변경이 발생되면 UI가 새로고침 됨
Fetch data : 데이터 가져오기
Retrofit 라이브러리를 이용해서 백엔드에 접근한다.
아래는 이전에 Retrofit2 라이브러리에 대해 정리한 포스팅이다!
interface Webservice {
/**
* @GET declares an HTTP GET request
* @Path("user") annotation on the userId parameter marks it as a
* replacement for the {user} placeholder in the @GET path
*/
@GET("/users/{user}")
suspend fun getUser(@Path("user") userId: String): User
}
ViewModel 구현을 위한
[ 1번째 아이디어 ]
Webservice를 직접 호출하여 데이터를 가져오고 이 데이터를 LiveData에 할당하는 것
- 효과가 있지만, 앱이 커지면서 유지관리가 어려워질 수 있음
- ViewModel 클래스에 너무 많은 책임을 부여하여 관심사 분리 원칙(Separation of concerns)에 위반
- ViewModel의 범위는 Activity 또는 Fragment 수명주기에 연결되어 있기 때문에 관련 UI 객체의 수명 주기가 끝나면 Webservice의 데이터가 손실됨 → 바람직하지 않은 User Experience를 만듦
[ 개선된 아이디어 ]
ViewModel에서 데이터를 가져오는 프로세스를 새로운 Repository 모듈에 위임하는 것
- Repository 모듈 : 데이터 작업을 처리함
- 나머지 앱에서 이 데이터를 간편하게 가져올 수 있음
- 데이터가 업데이트 될 때 어디에서 데이터를 가져올지나 어떤 API를 호출할지 알고 있음
- 지속되는 모델(persistent models), Web Service, Cache 등 다양한 데이터 소스 간의 중재자로 간주할 수 있음
Manage dependencies between components : 구성요소 간 종속성 관리
Repository에서 WebService를 통해 데이터를 가져오기 위해서는 WebService 객체가 필요함
객체 생성은 쉬우나 해당 클래스의 종속성이 필요하고, Repository는 WebService 외에도 필요한 클래스가 있을 수 있음
→ WebService의 참조가 필요한 각 클래스에서 해당 클래스와 종속성을 구성하는 방법을 알아야 하므로 코드를 복제해야 함
→ 클래스 별로 새로운 WebService를 만들면 앱의 리소스 소모량이 매우 커질 수 있음
class UserRepository {
private val webservice: Webservice = TODO()
// ...
suspend fun getUser(userId: String) =
// This isn't an optimal implementation because it doesn't take into
// account caching. We'll look at how to improve upon this in the next
// sections.
webservice.getUser(userId)
}
[ 문제 해결 방법 ]
- 종속성 주입 (Dependency Injection; DI) : DI를 사용하면 클래스가 자신의 종속성을 구성할 필요 없이 종속성을 정의할 수 있음
- Runtime 시에 다른 클래스가 이 종속성을 제공해야 함
- Android에서 DI를 사용하기 위해선 Dagger2 또는 Koin 라이브러리를 사용
- Service locator : 클래스를 구성하는 대신, 종속성을 얻을 수 있는 레지스트리를 제공하는 패턴
패턴은 코드를 복제하거나 복잡성을 추가하지 않아도 종속 항목을 관리하기 위한 명확한 패턴을 제공하므로 코드를 확장할 수 있음
또, 이러한 패턴을 사용하면 테스트 및 프로덕선 데이터를 가져오는 구현 간에 신속하게 전환할 수 있음
DI 패턴를 따르는 것과 Hilt 라이브러리를 사용하는 것을 추천한다.
Hilt 라이브러리는 dependency tree를 따라 이동하며 객체를 자동으로 구성하고 dependency의 컴파일 시간을 보장하며 Android 프레임워크 클래스의 dependency container를 만든다.
Cache data : 데이터 캐시
Repository의 구현은 WebService 객체 호출을 추출하지만 하나의 데이터 소스에만 의존하기 때문에 유연성이 떨어짐
Repository의 구현에서 발생하는 중요한 문제 : 백엔드에서 데이터를 가져온 후 어디에도 보관하지 않음
→ 사용자가 해당 UI를 나갔다가 다시 돌아오면 데이터가 변경되지 않았어도 앱에서 데이터를 다시 가져와야 함
- 네트워크 대역폭을 낭비함
- 새 쿼리가 완료될 때까지 사용자가 기다려야 함
Persist data : 데이터 지속
[ 기기를 회전하거나 앱에서 나갔다가 즉시 돌아오는 경우 ]
Repository가 메모리 내의 캐시에서 가져오기 때문에 기존 UI에 즉시 나타남.
[ Android OS에서 프로세스를 종료한 후에 다시 돌아오는 경우 ]
현재 상황에서는 네트워크에서 데이터를 다시 가져와야 함
→ 이 프로세스는 User Experience를 저해하고 귀중한 모바일 데이터를 소비하게 됨
∴ Room persistence library가 필요함
Room 라이브러리 : 개체 매핑 라이브러리
- 최소한의 보일러플레이트(boilerplate) 코드를 가지고 local data persistence를 제공함
- 컴파일 시에 데이터 스키마(Schema)에 대해 각 쿼리의 유효성을 검사하므로 SQL 쿼리 오류를 런타임 이전에 알 수 있음
- 데이터 베이스의 변경 사항을 LiveData 객체를 사용하여 관찰이 가능하여 변경사항을 즉시 반영할 수 있음
보일러플레이트(Boilerplate) : 코드가 반복되어 자주 쓰이지만 매번 작성하기 번거롭고 읽기 어려운 어려운 많은 양의 코드
권장사항
1. Activity, Service, Broadcast-receiver와 같은 앱의 진입점(entry point)을 데이터 소스로 지정하지 말 것
대신 해당 진입점과 관련된 데이터 일부만 가져오도록 다른구성요소에 맞춰 조정해야 함
각 앱 구성요소는 사용자와 기기의 상호작용 및 시스템의 전반적인 현재 상태에 따라 매우 단기간만 지속된다.
2. 앱의 다양한 모듈 간 책임이 잘 정의된 경계를 만들 것
여러 관련 없는 책임을 동일한 클래스에 정의하면 안되며, 어떠한 책임이 있는 코드를 여러 클래스나 패키지 전체에 분산시키면 안됨
3. 각 모듈은 가능하면 적게 노출할 것
하나의 모듈에서 내부 구현 세부정보를 노출하는 지름길을 만들면 안된다.단기적으로 약간의 시간을 벌 수는 있지만, 코드베이스가 발전함에 따라 기술적인 문제가 여러 번 발생할 수 있음
4. 각 모듈을 독립적으로 테스트 가능하게 만드는 법을 고려해야 함
두 모듈의 로직을 한 위치에 혼합하거나, 어떠한 기능 코드를 전체 코드베이스에 분산하면 테스트가 훨씬 어려워짐
5. 다른 앱과 차별되도록 앱의 고유한 핵심에 초점을 맞출 것
동일한 보일러플레이트 코드를 반복하여 작성하느라 시간을 낭비하지 말고 Android Architecture Components(AAC)와 권장 라이브러리를 사용하여 처리할 것. 대신 다른 앱과의 차별성을 위해 시간과 에너지를 집중할 것!
6. 가능한 관련성이 높고 최신의 데이터를 보존해야 함
이렇게 해야 기기가 오프라인 모드일 때도 사용자가 앱의 기능을 이용할 수 있음.모든 사용자가 끊김 없고 속도가 빠르 연결을 사용할 수 있는 환경에 있지 않다는 점을 유의할 것!
7. 하나의 데이터 소스를 단일 소스 Repository로 지정할 것
안드로이드 Jetpack을 공부해야 겠다고 생각하면서 공식 문서에서 아키텍처 가이드가 있어서 읽어보게 되었다. 지금까지 프로젝트를 하면서 위의 문서에서 말한 것처럼 UI 클래스에 코드를 몽땅 작성하는 실수도 해보았고, 이렇게 많은 코드를 어떻게 관리해야 할지에 대해 궁금증이 항상 있었다. 이 가이드라인을 읽는다고 해서 당장 이러한 아키텍처를 적용하기에는 무리가 있다고 생각한다. 줄글로 이해하는 것과 실제로 코드를 작성하는 것은 다르기 때문.. ㅠㅠ
그리고 공식문서를 어떻게 공부해야하나 여기저기 구글링할 때 한국어 번역보다 원문을 읽으라는 이야기가 많았는데, 오늘 확실히 깨달았다. 대표적으로는 Activity를 활동으로 표현하거나, 아직 미숙한 번역기로 어색한 문장들이 꽤 많았다. 원문을 참고하면서 읽었더니 이해가 훨씬 잘 됐다!
앞으로는 AAC와 MVVM에 대해 공부하고 실습하면서 Jetpack 라이브러리를 익혀볼 생각이다.
그리고 시간이 된다면 Jetpack Compose도 공부해보고 싶다!
'ANDROID > Android Jetpack' 카테고리의 다른 글
[Android/Jetpack] AAC - Data Binding (0) | 2021.08.11 |
---|---|
[Android/Jetpack] AAC - Room (0) | 2021.08.03 |
[Android/Jetpack] AAC - ViewModel (0) | 2021.07.27 |
[Android/Jetpack] AAC - LiveData (0) | 2021.07.21 |
[Android/Jetpack] AAC - Lifecycles (0) | 2021.07.20 |
[Android/Jetpack] MVVM 아키텍처와 AAC (0) | 2021.07.19 |
[Android] MVC, MVP 아키텍처 (0) | 2021.07.12 |
[Android/Jetpack] Jetpack이란? (0) | 2021.07.09 |
댓글