해당 포스팅은 Now In Android
의 [Modularization learning journey](https://github.com/android/nowinandroid/blob/main/docs/ModularizationLearningJourney.md)
의 영문을 번역한 버전입니다. 또한 직역이 아닌 의역을 했으므로 단어의 추가 삭제가 들어갔음을 말씀드립니다. 학습에 도움이 되셨으면 합니다 :)
이번 모듈화 학습 여정에서 우린 모듈화에 대해서 배울 것이다. 그리고 Now In Android
앱에서 모듈을 만드는데 있어 어떤 전략을 채택하는지 배울 것이다.
모듈화는 단일 모듈 코드베이스의 개념을 깨는 한 가지의 방법이며 앱 전체의 모듈 의존도를 느슨하게 만드는 방법입니다.
모듈화는 많은 이점을 제공합니다.
[확장성의 이점]
끈끈하게 결합된 코드베이스 내에선 한 부분의 변화가 여러곳의 변경을 초래할 수 있습니다. 적절하게 모듈화된 프로젝트는 관심사 분리를 향상시킬 수 있습니다. 즉, 이는 코드 작성자에게, 그리고 아키텍처 관점에서도 더욱 많은 자율성을 보장합니다.
[병렬 작업의 이점]
모듈화는 버전 관리의 충돌을 줄이는데도 용이합니다. 또한 커다란 팀 안에서 여러 개발자들이 작업하는데 있어서 효율적인 작업을 가능하게 도와줍니다.
[작업 주체 명확화의 이점]
모듈화는 기능 개발 및 테스트 코드의 작성과 버그 개선 그리고 변경을 검토하는데 있어서 개발자들 사이에서 집중해야할 부분을 명확히할 수 있습니다.
[캡슐화의 이점]
분리된 코드는 읽기 쉽고, 이해하고 테스트하고 유지하는데 더욱 쉽습니다.
[빌드 시간 단축의 이점]
Gradle
의 병렬 및 증분 빌드를 통해 빌드시간 단축이 가능합니다.
[동적 배포의 이점]
모듈화는 [Play Feature Delivery](https://developer.android.com/guide/playcore/feature-delivery?hl=ko)
의 요구사항입니다. 이는 앱의 특정 기능이 조건부로 배포되거나 요구사항에 맞게 다운로드될 수 있도록 합니다.
[재사용성의 이점]
적절한 모듈화는 코드 공유를 가능하게 합니다. 이는 한 가지의 코드베이스를 기준으로, 여러 플랫폼을 통해 각각의 앱이 빌드될 수 있도록 합니다.
하지만 모듈화는 오용되기 쉬운 패턴으로, 반드시 알아야할 지식이 있습니다.
[모듈을 너무 많이 나눴을 때]
각각의 모듈은 빌드 설정을 증가시킬 수 있으며 이는 비용입니다. 이는 Gradle
의 빌드시간을 증가시키며, 유지보수 비용을 증가시킬 수 있습니다. 추가로 모듈을 더욱 추가하는 것은 단일 모듈 프로젝트에 비해 Gradle
설정을 증가시키기도 합니다. 이런 문제의 해결책으로 convention plugin
으로의 마이그레이션을 고려할 수 있습니다. 또한 이는 Kotlin코드 베이스이기에 재사용 가능한 build script의 중복코드를 정리할 수 있습니다. Now In Android
앱에선 이러한 convention plugin
을 build-logic
폴더에서 찾아볼 수 있습니다.
[모듈을 충분히 못나눴을 때]
반대로, 만약 모듈 수가 적을 뿐만 아니라 방대한 양의 소스 코드들이 강하게 결합되어 있다면, 이는 결국 단일 모듈의 앱이 계속 될 가능성이 있습니다. 이는 모듈화의 이점을 제대로 활용하지 못하는 격입니다. 만약 모듈이 너무 큰데도 불구하고, 단일 책임을 가지지 않는다면 모듈 분리를 고려할 필요가 있습니다.
[모듈을 복잡하게 나눴을 때]
정답은 없습니다. 모든 프로젝트 각각에 있어, 모듈화를 진행하는 것이 의미가 있는건 아닙니다. 가장 중요한 요소는 코드의 크기와 복잡도입니다. 만약 당신이 진행하는 프로젝트가 특정 기준치 이상으로 커질것같지 않다면, 모듈화의 이점이 크지 않을 수 있습니다.
모든 프로젝트에 단일 모듈화 전략이 완전히 들어맞지 않는다는 사실을 명확히 인지할 필요가 있습니다. 하지만 모듈화의 이점을 최대한 활용하고 모듈 크기를 줄이는데 있어 시도해볼 수 있는 일반적인 가이드라인은 존재합니다.
모듈의 기본 단위는 Gradle
빌드 스크립트가 포함된 폴더입니다. 보통 모듈은 하나 이상의 소스 세트와 리소스 및 에셋의 조합으로 구성됩니다. 모듈은 독립적으로 빌드 및 테스트될 수 있습니다. 즉, 이를 통해 개발자는 낮은 결합도와 높은 응집도를와 생각하며 개발을 해야합니다.
[낮은 결합도]
모듈은 가능한 한 서로 독립적이어야 하며, 하나의 모듈 변화가 다른 모듈에 영향을 전혀 끼치지 않는 것을 목표로 해야합니다. 모듈들은 다른 모듈의 내부 동작에 대해 알면 안됩니다.
[높은 응집도]
하나의 모듈은 하나의 코드 덩어리가 시스템적으로 동작하도록 보장해야하며, 명확한 책임이 정의됨과 동시에 특정 도메인 지식의 범위 내에 존재해야만 합니다. 예를 들어, [core:network](https://github.com/android/nowinandroid/tree/main/core/network)
모듈은 네트워크 요청과 응답에만 책임이 있습니다. 그 후, 다른 모듈에 데이터를 전달하는 책임이 있습니다.
[TIP]
위와 같은 모듈 그래프는 모듈들 사이 의존 관계를 시각화해주므로 모듈화 설계 과정에서 매우 유용합니다.
Now In Android
앱에선 아래의 모듈들을 사용합니다.
[app]
앱 레벨의 코드를 포함합니다. 이를 Scaffolding
과 같은 여러 클래스를 포함합니다. 구체적인 예로 MainActivity
, NiaApp
, NiaNavHost
, BottomNavigation
, TopLevelDestination
과 같이 앱 레벨에서 제어 가능한 네비게이션 코드가 있습니다. 또한 app
모듈은 모든 feature
모듈에, core
모듈에 의존합니다.
[feature:]
기능 상세 모듈로써, 각각의 기능을 단일 책임 원칙으로 한정합니다. 이는 어떤 앱에서도(flavour타입 무관하게) 재사용할 수 있으며 테스트 또한 가능합니다. 만약 하나의 클래스가 단 하나의 feature
모듈에서만 필요하다면, 해당 클래스는 feature
모듈에 그대로 남아있어야 합니다. 하지만 그게 아니라면, 적절한 core
모듈로 추출될 수 있습니다. 이때, 하나의 feature
모듈은 다른 feature
모듈에 의존성을 갖지 않으며 오로지 core
모듈만 의존합니다.
[core:]
공통 라이브러리 모듈로써, 다른 모듈들 사이에서 공유될 수 있습니다. 이러한 모듈들은 다른 모듈들에 의존할 수 있지만, feature
또는 app
모듈에 의존하면 안됩니다.
[그 외 모듈들]
sync
, benchmark
, test
뿐만 아니라 app-nia-catalog
모듈을 의미합니다.
모듈 명 | 책임 | 핵심 클래스 및 예시 |
---|---|---|
app | UI Scaffolding이나 Navagation처럼 앱이 필요한 모든 것을 가져오는 책임 | NiaApp, MainActivity가 있으며, 앱 레벨에서 Navigation을 제어하는 NiaNavHost, NiaAppState, TopLevelDestination |
feature:1, feature:2 | UI/UX에 관련된 기능으로, feature:topic이 존재. 이는 TopicScreen에서 토픽 정보를 보여줌 | TopicScreen, TopicViewModel |
core:data | 다양한 소스로부터 데이터를 가져오며 이를 feature모듈에 전달하는 책임 | TopicRepository |
core:designsystem | Material3 컴포넌트로 커스터마이징된 핵심 UI 컴포넌트로, 앱 테마나 아이콘을 포함. 전체 디자인 시스템은 app-nia-catalog를 빌드함으로써 확인 가능 | NiaIcons, NiaButton, NiaTheme |
core:ui | feature모듈로부터 사용될 수 있는 혼합된 UI 컴포넌트와 리소스(ex. newFeed). designsystem 모듈과 달리, 이는 DataLayer에 해당하는데, 데이터를 렌더링하여 보여주기 때문이다. | NewsFeed, NewsResourceCardExpanded |
core:common | 모듈들 사이에서 공유될 수 있는 클래스 | NiaDispatchers, Result |
core:network | 원격 데이터 소스로부터 네트워크 요청과 응답을 생성 | RetrofitNiaNetworkApi |
core:testing | 종속성, Repository, 그 외 Util클래스를 테스트 | NiaTestRunner, TestDispatcherRule |
core:datastore | DataStore활용하여 영속 데이터를 저장 | NiaPreferences, UserPreferencesSerializer |
core:database | Room을 사용한 로컬 데이터베이스 | NiaDatabase, DatabaseMigrations, Dao 클래스들 |
core:model | 앱 전체에서 사용되는 모델들 | Topic, Episode, NewsResource |
앞서 소개드린 모듈화 전략은 Now In Android에 맞게 정의되었습니다. 추가적으로 우리들의 목표는 작은 앱을 과도하게 모듈화하는 것과 커다른 앱에 적절한 모듈화 패턴을 적용하는데 있어 균형을 잡는 것이며, 이를 통해 실 프로덕트 환경에 맞게 앱을 만드는데 있습니다.
이러한 논쟁은 안드로이드 커뮤니티에서도 진행중이며, 피드백을 받아 발전하고 있습니다. 하지만 위에서 소개한 모듈화 방법이 모든 문제에 대처하는 확실한 정답이 있는건 아닙니다. 궁극적으로 말하고 싶은 바는 모듈화에는 수많은 방법이 있으며, 모든 목적과 코드베이스와 팀에 있어 모든 경우에 적합한 한가지 방법이 있다는 것만은 아니라는 점입니다. 그렇기 때문에 사전에 목표 및 문제를 사전에 고려하고 잠재적인 문제를 예측하고 모듈 구조를 정의하는것이 반드시 필요합니다. 개발자는 모듈화에 대한 브레인스토밍을 통해 좋은 아이디어를 얻을 수 있으며, 이는 시각화를 통해 진행되므로 더욱 나은 방법일 수 있습니다.
우리 구글 팀의 접근법의 예시는 다음과 같습니다. 우린 모든 경우에 있어 불변 구조는 없다고 믿으며 앞으로도 끊임없는 요구사항에 따라 변할 수 있다는 점입니다. 위에서 보여준 모듈화 전략은 Now In Android
에 적합하게 고안된 일반적인 가이드라인입니다. 그리고 이를 당신이 더욱더 수정하거나 확장할 수 있습니다. 이를 수행하는 한 가지 방법은 코드베이스를 더욱 세분화하는 것입니다. 이러한 세분성에 따라서 당신의 코드를 구성할 수 있습니다. 예를 들어, 만약 당신의 DataLayer
가 작다면, 이를 하나의 모듈로 두는 것도 괜찮은 방법입니다. 하지만, Repository
와 DataSource
가 많아지기 시작하면, 세부적인 모듈로 나누는 것도 좋은 방법입니다.
우린 항상 당신의 건설적인 피드백을 기다립니다. 커뮤니티로부터 얻은 지식이나 변경됐으면 하는 좋은 아이디어는 우리들의 가이드라인을 발전시키는 좋은 방법입니다.