Android App architecture: Common patterns

안드로이드 App architecture: Common patterns를 알아보겠습니다.





출처:
https://developer.android.com/topic/modularization/patterns



모든 프로젝트에 적합한 modularization(모듈화)을 위한 하나의 전략은 없습니다. Gradle의 자유로운 성질 때문에 적은 제약사항으로 project(프로젝트)를 묶어야 합니다. 이 문서에서는 일반적인 규칙과 흔히 사용되는 patterns(패턴)를 전체적으로 다뤄 당신의 multi(멀티) module Android 앱에 적용할 수 있게 도와줍니다.





• 메모:

이 문서에 있는 추천과 좋은 사례들은 다양한 방면으로 앱의 확장 가능성, 품질 향상, 굳건함, 테스트하기 쉬움을 제공해 줍니다. 하지만, 당신은 이것을 지침으로 사용하고 당신이 필요할 때에만 적용하기를 권장합니다.





⊙ High cohesion and low coupling principle: 높은 응집력 그리고 낮은 연결성 원칙;

Modular 코드를 설명하는 하나의 방법으로는 coupling(연결)과 cohesion(응집)을 사용해서 설명하는 것입니다. Coupling은 modules가 다른 modules를 참고하는 정도를 나타내고, Cohesion은 여기서는 하나의 module의 요소가 얼마나 연관되어 있는지를 나타냅니다. 일반적인 규칙으로는 당신은 낮은 coupling과 높은 cohesion을 구현하도록 노력해야 합니다.



• 낮은 coupling은 modules 가능하다면 다른 것들과 독립적이어야 한다는 뜻입니다. 따라서 하나의 module을 수정한다면, 0 또는 가능한 적게 다른 modules에 영향을 줘야 합니다. Modules는 다른 modules의 내부 동작을 알 필요가 없습니다.



• 높은 cohesion은 시스템처럼 동작하는 코드의 모음을 의미합니다. 이것은 명확하게 정의된 역할을 수행하며, 특정한 domain(분야) 안에서만 동작합니다. Ebook을 예시로 들어봅시다. 하나의 module 안에 책과 결제 관련 코드가 함께 섞여있는 것은 적절하지 못합니다. 왜냐하면 두 개는 서로 다른 domain의 기능들이기 때문입니다.





• 팁:

만약 두 개의 modules가 강력하게 서로를 안다면, 두 개를 하나의 시스템으로 묶을 수 있다는 좋은 신호입니다. 반대로 module의 두 부분이 서로 상호작용을 적게 한다면, 다른 module로 분리할 수 있다는 의미가 됩니다.





⊙ Types of modules: Modules의 종류;

당신의 modules를 구성하는 주된 방법은 당신의 앱 architecture(구조)에 맞추는 것입니다. 아래의 몇 가지 일반적인 types의 modules은 당신의 앱이 우리의 recommended app architeuctre를 사용한다면 당신의 앱에 사용할 수 있습니다.



• 메모:

다음 영역은 당신이 app architecture에 익숙하다고 가정하고 진행합니다.





• Data modules: 데이터 modules;

Data module은 보통 repository, data sources(데이터 소스) 그리고 model(모델) classes(클래스)를 가집니다. 3가지 주된 data module의 역할은 아래와 같습니다.



  1. Encapsulate all data and business logic of a certain domain: 모든 data와 특정한 domain의 business(비즈니스) logic(논리)을 캡슐화함;

각각의 data module은 특정한 domain의 data를 관리하는 역할을 가집니다. 자신과 관계된 많은 types의 data를 다룹니다.



  2. Expose the repository as an external API: repository(레포지터리)를 외부 API로 노출;

Data module의 public(공개) API는 나머지 앱에 data를 노출하는 역할을 가진 repository를 사용합니다.



  3. Hide all implementation details and data sources from the outside: Data source와 사용된 기술들을 모두 밖에서 볼 수 없게 숨김;

Data source는 무조건 같은 module의 repositories에서만 접근할 수 있어야 합니다. Data sources는 밖에서 볼 수 없어야 합니다. 당신은 이것을 Kotlin의 private나 internal로 visibility(가시성)를 강제할 수 있습니다.






• Feature modules: 기능 modules;

Feature은 화면 또는 연관된 화면에 연관 있고 격리된 앱 기능입니다. 예를 들면, 회원 가입과 확인 flow(흐름)이 있습니다. 만약 당신의 앱에 bottom bar navigation이 있다면, 각각의 화면은 feature가 됩니다.





• 중요 용어:

"Feature module"은 Play Feature delivery의 조건부 전달 또는 필요할 때만 다운로드 module에서도 사용되는 용어입니다.  그러나, 이 지침에서 사용하는 뜻은 당신의 앱의 기능을 구분 짓는 캡슐화 module을 의미합니다.








Features는 당신의 앱에서 일반적인 화면이나 도착지 화면입니다. 그러므로 Features는 logic과 state(스테이트)를 관리하기 위한 UI와 ViewModel과 연결되어 있습니다. 하나의 feature는 하나의 view나 navigation destination을 가져야 한다는 제약은 없습니다. Feature module은 data module에 종속됩니다.







• App modules: 앱 modules;

App(앱) modules는 application(애플리케이션)의 진입점입니다. 이들은 feature modules에 종속적입니다. 그리고 root(부모) navigation을 제공합니다. 하나의 app module은 수많은 종류의 바이너리로 compile(컴파일) 될 수 있습니다. 고마운 기능인 build variants!








만약 당신의 앱이 auto, wear 또는 TV 등 다양한 기기 types을 지원한다면, app module에서 각각 정의합니다. 이것은 특정한 platform(플랫폼)의 종속성을 나누는 데 도움을 줍니다.








• Common modules: 일반적인 modules;

Common(일반적인) modules는 core(핵심적인) modules로도 알려져 있으며, 다른 modules에서 자주 사용하는 코드가 들어있습니다. common modules는 불필요한 중복을 없애고 어떠한 앱 architecture의 어떠한 layer도 대표하지 않습니다. 아래는 common modules에 대한 예시입니다.



  - UI module:;

만약 당신이 수정한 UI 요소나 브랜드를 설명하기 위한 UI 요소가 있다면,  당신은 모든 features에서 재사용 할 수 있게 해당 부분들을 module로 만들어 캡슐화할 것을 고려할 수 있습니다. 이것은 다른 features에서도 UI를 일관성 있게 유지하게 해줍니다. 예를 들어, 당신의 theme이 관리되고 있다면, 당신은 브랜드를 변경할 때 고통스러운 refactor(리펙터)를 피할 수 있습니다.



  - Analytics module:분석 module;

추적은 business 요구 사항에서 요청되며 소프트웨어의 architecture에서는 사소하게 고려됩니다. 분석 추적은 보통 관련 없는 여러 요소에서 사용됩니다. 만약 당신의 경우가 이렇다면, 분석을 전담하는 module을 만드는 것은 좋은 생각이 될 수 있습니다.



  - Network module: 네트워크 module;

많은 modules가 네트워크 연결이 필요할 때, http client(클라이언트)를 제공해 주는 역할에 충실한 module을 고려해 볼 수 있습니다. 이것은 특히 당신의 client가 변경된 설정이 필요할 때 유용합니다.



  - Utility module: 유틸리티 module;

Utilities 또는 helpers라고 알려진 것은 앱 전반적으로 재사용되는 작은 코드 조각입니다. Utilities의 예로는 테스트 helpers, 통화 단위 함수, 이메일 확인 또는 비슷한 함수들이 있습니다.




• Test modules: 테스트 modules;

Test modules는 테스트를 위한 목적으로 만드는 Android modules입니다. 이 modules는 테스트 code를 포함하고, 테스트 resources 그리고 앱 실행이 아닌 테스트 동안 필요한 dependencies를 제공합니다. 테스트 modules는 앱과는 분리된 test 목적의 코드를 만듭니다. module 코드를 더 쉽게 관리하고 유지 보수하게 만듭니다.



- Use cases for test modules: 테스트 modules를 위한 사례;

아래의 예제는 테스트 module을 사용한 상황의 이점을 보여줍니다.



  = Shared test code: 테스트 코드 공유;

만약 당신은 많은 modules를 project에 사용 중이고, 몇몇 테스트 코드가 하나 이상의 module에 적용 가능할 때, 테스트 module을 만들어서 코드를 공유할 수 있습니다. 이것은 중복을 줄여주고, 테스트 코드의 유지 보수를 쉽게 만들어 줍니다. 공유된 테스트 코드는 utility classes 또는 함수가 포함될 수 있습니다. 예를 들면, 변경된 assertions 또는 matchers 그리고 가상화된 JSON 응답 같은 테스트 data가 있습니다.



  = Cleaner Build Configurations: 깨끗한 Build 설정;

테스트 modules는 build 설정을 깨끗하게 만들어줍니다. test modules는 자신의 build.gradle을 가지기 때문에 앱의 build.gradle에는 test 관련된 설정을 없앨 수 있습니다.



  = Integration Tests: 통합 테스트;

테스트 modules는 앱의 다른 부분과 상호작용하는 테스트를 통합하는 곳으로 사용할 수 있습니다. 인터페이스, business logic, 네트워크 요청, database queries(쿼리) 등을 통합합니다.



  = Large-scale applications: 큰 규모의 앱;

테스트 modules는 많은 modules를 가진 복잡한 큰 앱에 특히 유용합니다. 이러한 경우에는 테스트 modules는 코드의 조합과 유지 보수성 향상을 가져다줄 수 있습니다.







⊙ Module to module communication: module 간의 통신;

Modules는 희박하게 다른 modules와 통신하고 연관되어 있습니다. 모듈이 함께 작동하며 정보를 자주 교환하더라도 coupling을 낮게 유지하는 것이 중요합니다. 가끔 직접적으로 두 modules끼리 통신하는 경우가 발생하는데 이는 architecture 제약 상황에 따라 존재해서는 안 됩니다. 순환 참조를 발생시키기 때문에 절대로 있어선 안됩니다.







이러한 문제를 해결하기 위해, mediating(중간자) 역할을 하는 제3의 module이 필요합니다. mediator module은 양쪽에서 오는 메시지를 들을 수 있고, 필요한 곳으로 전달할 수 있습니다. 우리의 예제 앱에서는 checkout 화면이 어떠한 book을 샀는지 알 필요가 있습니다. 비록 이벤트가 다른 feature의 부분에서 발생했더라도 말이죠. 이러한 경우, mediator는 navigation graph를 가진 module(보통 app module)입니다. 예제에서는 navigation을 사용하여 data를 home feature에서 checkout feature로 넘기는데, Navigation component를 사용합니다.








도착지인 checkout은 책 id를 argument(전달 인자)로 받고 book에 대해서 정보를 최신화합니다. 당신은 saved state handle을 사용하여 navigation argument를 도착 feature의 ViewModel에 저장할 수 있습니다.





당신은 objects(오브젝트)를 navigation arguments로 전달해서는 안 됩니다. 대산에 간단한 ids를 전달하여 원하는 정보를 data layer에서 불러올 수 있게 합니다. 이 방법은 single source of truth 원칙을 지키며, coupling을 낮춰줍니다.



아래의 예시는 양쪽의 feature modules에서 같은 data module에 종속되는 것을 보여줍니다. 이것은 mediator module이 전달하는 data의 양을 줄이고, coupling을 낮게 유지시켜줍니다. Objects를 전달하는 대신에 대표적인 IDs를 전달하고, 공유된 data module에서 해당 자료를 가져오도록 만들어야 합니다.







⊙ Dependency inversion: 의존성 역전;

Dependency inversion(역전)은 당신의 코드를 조직할 때, 구체적인 것을 추상화하는 것입니다.



• Abstraction: 추상화;

당신의 앱에서 요소와 modules 간에 얼마나 상호작용하는지를 정의합니다. Abstraction modules는 시스템 API나 interfaces와 modules를 정의합니다.



• Concrete implementation: 구체적인 적용;

Abstraction module에 종속된 modules과 abstraction의 동작을 적용한 module이 있습니다.






• Example: 예시;

작동하기 위해 database가 필요한 feature module이 있다고 생각해 봅시다. Feature module은 database가 어떻게 적용되는지 관심이 없습니다. database는 Room database 일 수 있고, 원격 Firestore instance 일 수도 있습니다. 단지 앱의 data를 읽을 수 있기만 하면 됩니다.



이것을 구현하기 위해서는 feature module은 직접적으로 database를 적용한 module이 아닌 abstraction module과 종속됩니다. 이 abstraction에는 앱의 database API가 정의되어 있습니다. 쉽게 말하자면, 어떻게 database와 상호작용할 것인지에 대한 규칙이 정해져있습니다. 이것은 feature module이 적용된 상세 기술을 모르더라도 어떠한 database라도 사용할 수 있게 해줍니다.



concrete(구체적) 구현 module은 abstraction module에 있는 APIs를 실제로 구현한 APIs를 제공해 줍니다. 이것을 하기 위해서는 구현한 module 또한 abstraction module에 종속되어야 합니다.



• Dependency injection: 의존성 주입;

당신은 어떻게 feature module과 구현 module이 연결되는지 궁금할 수 있습니다. 해답은 바로 Dependency Injection입니다. Feature module은 database instance를 직접적으로 만들지 않습니다. 대신에 어떠한 dependencies가 필요한지 적습니다. 이러한 dependencies는 외부에서 공급을 받는데, 대부분 app module에서 제공해 줍니다.







• 메모:

당신은 다양한 dependencies를 build types마다 정의할 수 있습니다. 예를 들면, release(출시) build에서는 Firestore를 적용할 수 있고, debug build에서는 내부 Room database를 사용할 수 있습니다. 그리고 테스트에서는 mock(가짜)를 적용할 수 있습니다.







• Benefits: 이점;

당신의 API를 나누고 적용하는 것의 이점은 다음과 같습니다.



  - Interchangeability: 교환 가능성;

명확하게 분리된 API와 implementation(적용) modules는 같은 API를 여러 군데 사용할 수 있게 해줍니다. 그리고 코드의 변경 없이 서로 교환 가능하게 만듭니다. 이것은 특정한 상황에 따라 다른 행동이나 실행 범위를 정할 때 좋습니다. 예를 들면, 실제 제품 API를 적용하는 대신 테스트를 위한 mock를 적용하는 것입니다.



  - Decoupling: 연결 해제;

분리라는 말은 추상화를 통해 어떠한 기술에도 종속되지 않는다는 것을 의미합니다. 만약 당신이 먼 훗날 Room 대신 Firestore의 database를 사용하게 된다면 implementation module 변경 작업만 하면 되기 때문에 쉽습니다. 또한, 다른 module에서 database API를 사용하는데 아무런 영향을 주지 않습니다.



  - Testability: 테스트가 쉬움;

APIs를 나누는 것은 테스트하기 쉽게 만들어줍니다. 당신은 API 명세서에 따라 테스트를 작성할 수 있습니다. 또한 당신은 각기 다른 implementations를 사용하여 여러 상황과 비정상적인 상황, mock를 적용하는 등의 방법으로 테스트할 수 있습니다.



  - Improved build performance: build 향상;

각기 다른 modules로 API를 나누면 implementation module의 코드 변경은 종속된 module의 recompile(다시 컴파일)을 강제로 실행하지 않습니다. 이것은 빠른 build 시간을 제공해 주며, 생산성 향상이 됩니다. 특히 큰 projects의 경우 build 시간은 더욱 중요합니다.





• When to separate: 언제 분리할까;

아래의 상황에서 implementations의 API를 분리하는 것이 좋습니다.



  - Diverse capabilities: 사용 가능성의 다각화;

다양한 방법으로 implement 부분을 적용할 수 있다면, API는 다른 implementations로 교환이 가능합니다. 예를 들면, 화면에 구현하기 위한 OpenGL 또는 Vulkan 선택 가능, Play를 사용한 계산 시스템 또는 당신이 만든 계산 시스템을 선택하여 사용할 수 있습니다.



  - Multiple applications: 다양한 앱;

다른 platforms(플랫폼)에서도 공유되고 작동 가능한 앱을 만든다면, 당신은 common APIs와 특정한 implementations를 각 platform마다 정의할 수 있습니다.



  - Independent teams: 독립된 팀;

분리는 다른 개발자 또는 다른 팀이 동시에 다른 코드 부분을 수정할 수 있게 해줍니다. 개발자는 API 명세서를 이해하는데 집중할 수 있고, 다른 module의 자세한 사항에 대해서 걱정할 필요가 없어집니다.



  - Large codebase: 양이 많은 코드;

코드가 양이 많거나 복잡하다면, API를 implementation에서 나누는 것은 코드를 관리하기 쉽게 만듭니다. 코드를 잘게 나눌수록 쉽게 이해 가능하고, units(유닛) 단위로 유지 보수가 가능해집니다.





• How to implement?: 어떻게 적용하는가?;

Dependency inversion을 적용하기 위해서는 다음 단계를 따르세요.



  1. Create an abstraction module: 추상화 모듈을 만듭니다.

이 module은 interfaces나 models를 가진 APIs를 가집니다. APIs는 feature의 행동이 정의되어 있습니다.



  2. Create implementation modules: implementation module을 만드세요.

Implementation modules는 API module과 연관이 있고, abstraction의 행동을 implement 합니다.







  3. Make high level modules dependent on abstraction modules: 높은 수준의 module을 만들고 abstraction modules에 종속합니다.

특정한 implementation에 직접 종속되는 대신, 당신의 modules를 abstraction modules에 종속되게 합니다. 높은 수준의 modules는 implementation의 자세한 내용을 알 필요가 없고 단지 API만 알면 됩니다.







  4. Provide implementation module: implementation module을 제공하세요.

마지막으로 당신은 실제 implementation을 dependencies로 제공해 줘야 합니다. Project 설정과 관련된 특정한 implementation은 (app module이 설정을 진행하기 좋은 곳입니다) implementation build 설정 또는 테스트 source에 따라 특정한 dependency를 제공해 줘야 합니다.







⊙ General best practices: 일반적인 좋은 사례들;

앞에서 언급했듯이 multi-module에 맞는 하나의 방법은 없습니다. 많은 software의 architecture가 있듯이 많은 앱 modularize 방법이 존재합니다. 그럼에도, 아래에 소개할 일반적인 추천들은 코드를 읽기 쉽게 만들고, 유지 보수를 쉽게 만들고, 테스트를 쉽게 만듭니다.



• Keep your configuration consistent: 당신의 설정을 일관되게 만드세요;

모든 module은 configuration 업무 과부하를 일으킵니다. modules이 특정한 개수 이상이 되면, 설정을 모두 일치하게 만드는 작업이 힘들게 느껴집니다. 예를 들어, modules이 모두 같은 dependency를 사용하는 것이 중요합니다. 만약 당신이 모든 modules의 library를 단지 version 업데이트를 한다고 하면, 힘들 뿐만 아니라 실수를 할 수 있게 됩니다. 이러한 문제를 해결하기 위해, 하나의 gradle tools(툴)를 사용하여 설정을 관리할 수 있습니다.



  - Version catalogs는 sync 동안 만들어지는 Gradle에서 만드는 type safe 한 목록입니다. 이것은 project의 모든 modules를 정의하고 관리하는 곳입니다.

  - modules 간의 build logic을 공유하는 convention plugins 사용





• Expose as little as possible: 최소한만 노출하기;

module의 public interface는 최소한으로 유지하고 필요할 때만 노출합니다. 이것은 implementation의 상세한 내용을 밖으로 누출하지 않는 방법입니다. 모든 것을 가능한 작게 만듭니다. Kotlin의 private나 internal visibility를 사용하여 module을 private 하게 만듭니다. module 안에서 dependencies를 정의하면, api보다는 implementation을 사용하길 권장합니다. api는 module을 사용하는 모든 곳에 dependencies를 노출합니다. Implementation 사용은 build 시간 향상에도 도움을 줄 수 있습니다. 왜냐하면 다시 build 하는 것을 줄여주기 때문입니다.





• Prefer Kotlin & Java modules: 선호하는 Kotlin과 Java modules;

여기에는 Android studio에서 지원하는 3가지 types의 modules가 있습니다.



  - App modules: 앱 modules;

App modules는 앱의 시작 지점입니다. source 코드를 가지거나, resources, assets(에셋) 그리고 AndroidManifest.xml을 가집니다. app module의 결과물은 Android App Bundle (AAB) 또는 Android Application Package (APK)입니다.



  - Library modules: 라이브러리 modules;

Library modules는 app modules와 같은 내용을 가집니다. 다른 Android modules에서 dependency로 사용됩니다. library module의 결과물은 Android Archive (AAR)이며, 구조적으로 app modules와 구분되는 점도 있지만, 다른 modules에서 dependency로 사용할 수 있게 Android Archive (AAR) 파일로 컴파일 되는 다른 점도 있습니다. Library module은 같은 logic을 캡슐화하고 재사용 가능하게 하며, 많은 module에 resources로 사용됩니다.



  - Kotlin and Java libraries: Kotlin과 Java 라이브러리;

Kotlin과 Java library는 어떠한 Android resources, assets 또는 manifest files를 가지지 않습니다.



Android modules가 업무의 과중화가 발생할 수 있으므로, Kotlin이나 Java 종류를 가능한 사용하는 것이 좋습니다.





끝.



카테고리: Android

댓글

이 블로그의 인기 게시물

Python urllib.parse.quote()

Python bytes.fromhex()

Python OpenCV 빈 화면 만들기

Android Notification with Full Screen

Android Minimum touch target size

Android Compose Instrumentation test to unit test

tensorflow tf.expand_dims()

KiCad 시작하기 7 (FreeRoute 사용하기 2)

딩기 요트 명칭

Forensics .pyc 파일 .py로 복구하기