brunch

You can make anything
by writing

C.S.Lewis

by 이승현 Sep 08. 2022

앱 아키텍처 가이드

구글의 가이드를 읽고..

Overview


구글에서 권장하는 '앱 아키텍처 가이드' 를 살펴하고, 샘플들을 비교해본다.





모바일 앱 사용자 환경


OS 에 의해 언제든지 앱 프로세스가 종료될 수 있다.

→ 이러한 이벤트는 직접 제어할 수 없다.

앱 구성요소에 데이터나 상태를 저장해서는 안되며, 서로 종속되면 안 된다.




일반 아키텍처 원칙


앱을 확장하고 견고성을 높이며, 더 쉽게 테스트할 수 있도록 '아키텍처' 를 정의해야 한다.


앱 아키텍처란?

기능 간의 경계를 정의




관심사 분리


가장 중요한 원칙은 '관심사 분리' 이다.


Activity, Fragment

→ OS 와 앱 사이의 계약을 나타내도록 이어주는 클래스일 뿐이다.

→ 언제든지 OS 에 의해 제거될 수 있다.

이러한 클래스에 대한 의존성을 최소화해야 한다.

→ UI 및 OS 상호작용을 처리하는 로직만 포함해야 한다.




데이터 모델에서 UI 도출하기


데이터 모델에서 UI 를 도출해야 한다.

→ 데이터 모델은 앱의 데이터를 나타낸다.

→ UI 요소 및 기타 구성요소로부터 독립적이다.

→ 생명 주기와 관련 없다.




단일 소스 저장소


데이터 추가, 변경, 삭제는 단일 소스 저장소를 통해서만 가능하다.

모든 변경사항을 한곳으로 일원화한다.

다른 유형이 조작할 수 없도록 데이터를 보호한다.

데이터 변경사항을 더 쉽게 추적할 수 있어, 버그를 발견하기 쉽다.


단방향 데이터 흐름


상태와 데이터 흐름은 한 방향으로만 흐른다.

데이터의 일관성을 강화하고, 오류가 발생할 확률이 줄며, 디버그가 쉽다.




권장 앱 아키텍처





UI 레이어



화면에 데이터를 표시하고 사용자 상호작용의 기본 지점



UI 상태

→ 앱에서 사용자가 봐야 한다고 지정하는 항목

→ UI 상태가 변경되면 즉시 UI 에 반영된다.



UI 상태는 val 이기 때문에, 변경할 수 없다.

→ 불변성으로 인해, 객체가 순간의 상태를 보장한다.

변경이 불가능한 스냅샷




상태 홀더


UI 상태를 생성하는 클래스

일반적으로 ViewModel 의 인스턴스



UI 와 ViewModel 간 상호작용은 대체로 '이벤트 입력' 과 입력의 후속 상태인 '출력' 으로 간주된다.


상태가 아래로, 이벤트는 위로 향하는 패턴을 단방향 데이터 흐름(UDF) 라고 한다.  

UI 상태는 ViewModel 에 의해 변환된 애플리케이션 데이터

UI 가 ViewModel 에 사용자 이벤트를 아린다.

Viewodel 이 사용자 작업을 처리하고 상태를 업데이트 한다.

업데이트된 상태가 렌더링할 UI 에 대시 제공된다.

상태 변경을 야기하는 모든 이벤트는 위의 작업이 반복된다.




UI 상태 노출


상태를 스트림으로 간주할 수 있다.

→ LiveData 또는 StateFlow 를 통해 상태를 노출한다.



LiveData vs StateFlow


[공통점]
- 둘 다 관찰 가능한 데이터 홀더 클래스
- 앱 아키텍처에 사용할 때 비슷한 패턴을 따른다.

[차이점]
- StateFlow의 경우 초기 상태를 생성자에 전달해야 하지만 LiveData의 경우는 전달하지 않는다.

- 뷰가 STOPPED 상태가 되면 LiveData.observe()는 소비자를 자동으로 등록 취소하는 반면, StateFlow 또는 다른 흐름에서 수집하는 경우 자동으로 수집을 중지하지 않는다.
동일한 동작을 실행하려면 Lifecycle.repeatOnLifecycle 블록에서 흐름을 수집해야 합니다.

- 안드로이드 플랫폼에 종속적이었던 LiveData 와는 달리, StateFlow 는 순수 kotlin 라이브러리이기 때문에 Domain Layer 에서 사용할 수 있다


UiState 객체에 필드가 많을수록 스트림이 내보내질 가능성이 크다.

→ distinctUntilChaged() 를 통해 완화 작업이 필요할 수 있다.




ViewModel 일회성 이벤트 안티패턴


VIewModel 의 이벤트는 즉시 UI 상태 업데이트로 소비되어야 한다.

→ Channel 이나 SharedFlow 같은 다른 옵션을 이용하면, 이벤트 전달 및 처리가 보장되지 않는다.



UI 가 백그라운드에 진입하면, Channel collection 이 멈추고, 이는 이벤트를 놓치게 된다.




Domain 레이어


복잡한 비지니스 로직이나 여러 ViewModel 에서 재사용되는 간단한 비지니스 로직의 캡슐화

→ 모든 앱에 이런 요구사항이 있는 것이 아니기 때문에, 선택사항


코드 중복을 방지

테스트 용이함

책임을 분할하여 대형 클래스 방지


이를 가볍게 유지하기 위해, 기능 하나만 담당하고 변경 가능한 데이터는 UI 또는 데이터 레이어에서 처리해야 한다.




이 가이드의 이름 지정 규칙


각각 담당하고 있는 단일 작업에 따라 지정된다.


현재 시제의 동사 + 명사/대상(선택사항) + UseCase      




종속 항목





재사용 가능한 로직을 포함하기 때문에, 다른 UseCase 에 의해 이용될 수 도 있다.




Data 레이어


비지니스 로직을 포함하고 데이터를 노출



Repositories

데이터 노출
→ 일관된 상태를 위해, 이 데이터는 변경 불가능해야 한다.

데이터 변경사항을 한 곳에 집중

여러 데이터 소스 간의 충돌 해결

비지니스 로직 포함





대표 비지니스 모델


데이터 소스에서 가져오는 정보 모두가, 앱에 필요한 건 아니다.


따라서 모델 클래스를 분리해여 관리한다.  

필요한 수준으로 데이터를 줄여 앱 메모리를 절약

앱에서 사용하는 데이터 유형에 맞게 외부 데이터 유형으로 조정

관심사를 분리




구성요소 간 종속 항목 관리


의존성 주입 (DI)

→ 클래스가 자신의 종속 항목을 구성할 필요 없이 종속 항목을 정의할 수 있다.

→ 런타임 시 주입


서비스 로케이터

→ 클래스가 자신의 종속 항목을 구성하는 대신, 종속 항목을 가져올 수 있는 레지스트리를 제공한다.




아키텍처의 이점


전반적인 유지관리성, 품질, 견고성이 개선된다.

확장할 수 있따. 코드 충돌이 최소화되어 더 많은 인력과 팀이 동일한 코드베이스에 기여할 수 있다.

온보딩에 도움이 된다. 프로젝트에 일관성을 부여해 새로운 팀원이 빠르게 업무를 시작하고 보다 짧은 시간에 효율을 높일 수 있다.

테스트하기 더 쉽다.

잘 정의된 프로세스를 이용하여 버그를 체계적으로 조사할 수 있다.




아키텍처 비교


https://proandroiddev.com/multiple-ways-of-defining-clean-architecture-layers-bbb70afa5d4a


#01 Single module / 3 packages


고전적인? 방법이다.


서로 간 의존성을 분리할 강제적인 방법이 없다.
→ 한 모듈 안에 있기 때문에, 어디서든 접근 가능


확장이 힘들다.
 앱이 커져도 단일 모듈에 모두 있다.


#02 3 modules



Presentation (UI) 모듈이 app 모듈 역할

첫 번째 방법보다는 더 낫지만, 여전히 문제가 있다.

'Feature' 단위로 변경이 있으면, 여러 모듈을 모두 수정해야 하며, 이 과정에서 놓치거나 의존성을 추가할 수 있다.


#03 Feature modules / 3 packages



app 모듈이 모든 모듈을 연결한다.

Feature 관점에서 적절한 코드 분리가 되었다.

담당자 별로 적절한 Feature 모듈을 소유할 수 있다.

의존성을 완전히 분리할 순 없다.
→ 다른 계층간 import 문이 있는지 검사하는 방식으로 검사할 순 있다.

저자는 이 방식이 가장 이상적이라고 본다.


Base module 이 필요하다. 
→ Base UI component 나 util 들을 정리


UI 테스트는 어떻게 하나?
→ UI 일 경우, 단일 화면은 각 Feature 모듈에서 가능하지만 여러 화면은 불가능하다.
→ 이 경우엔 app 모듈에서 테스트 해야 한다.


#04 Feature modules * 3 modules

 



모듈이 너무 많아 진다.

→ 모듈간 의존성 지옥에 빠질 수 있다.




Layer separation enforced by Custom lint rule

Domain has Android framework dependency : Yes

확장성 : Low


Layer separation enforced by Module dependencies (IDE & compiler)

Domain has Android framework dependency : No

확장성 : Medium


Layer separation enforced by Custom lint rule

Domain has Android framework dependency : Yes

확장성 : High



Layer separation enforced by Module dependencies (IDE & compiler)

Domain has Android framework dependency : No

확장성 : Low




모든 프로젝트에 잘 맞는 이상적인 방식은 없다!


첫 번째 방식으로 개발하다가 점차적으로 2번째, 3번째 방식으로 마이그레이션하면 좋겠지만, 이는 쉬운게 아니다.

적절하게 분리된 Feature 로 프로젝트를 시작하는 게, 비용면에서 더 효과적이다.




샘플


#01 Now in Android



[app module]

MainActivity, MainApplication 및 App 레벨에서 다루는 코드 포함

NiaNavHost, NiaTopLevelNavigation 등


[feature modules]

단일 책임 모듈

분리 및 격리 상태 유지 (이대로 다른 앱에서도 이용 가능)

만약 다른 모듈에도 필요한 클래스라면, core 모듈 로 따로 뺀다.

다른 feature 모듈에는 의존하면 안되고, core 모듈에만 의존해야 한다.


[core]

공통 라이브러리 모듈

다른 core 모듈에 의존해도 된다. feature 나 app 모듈엔 의존하면 안된다.


[기타]

sync, benchmark, test 등


#02 architecture-templates


[app module]

MainActivity, MainApplication 및 App 레벨에서 다루는 코드 포함

Navigation


[feature modules]

단일 책임 모듈

분리 및 격리 상태 유지 (이대로 다른 앱에서도 이용 가능)

만약 다른 모듈에도 필요한 클래스라면, core 모듈 로 따로 뺀다.

다른 feature 모듈에는 의존하면 안되고, core 모듈에만 의존해야 한다.


[core]

공통 라이브러리 모듈

다른 core 모듈에 의존해도 된다. feature 나 app 모듈엔 의존하면 안된다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari