이번 포스팅에서는 now in android 프로젝트의 모듈 구조, 모듈화에 대해서 작성할 것이다.
모듈화는 모놀리틱 방식의 컨셉을 탈피하고, 한 가지 모듈의 코드베이스에서 낮은 결합도(loosely coupled) 방식의 코드 구현을 지향한다.
확장성 - 강하게 커플링된 코드에서는, 간단한 변화 하나가 연속적인 변화를 유발할 수 있다. 적절히 모듈화된 프로젝트는 separation of concerns 원칙을 포함한다.
소프트웨어 아키텍처 전공 수업에서 교수님이 항상 강조하시던 구절이었다.
이것은 아키텍처 패턴을 강제하는 와중에도 컨트리뷰터들에게 더 많은 자율성을 부여해준다.
병렬적인 작업 수행 가능 - 모듈화는 버전 컨트롤 충돌(git merge를 하다가 충돌나는 경우)을 감소할 수 있으며, 더 큰 조직의 개발자들로 하여금 더 효율적인 병렬적인 작업을 수행 가능하도록 한다.
소유권 - 모듈은 해당 코드에 대해 테스트를 진행하고, 버그를 수정하고, 변화를 리뷰하고, 유지보수할 책임을 오너에게 줄 수 있다.
캡슐화 - 독립적인 코드는 더 읽기 쉽고, 이해하기 쉽고, 테스트하기 쉽고, 유지보수하기에 쉽다.
빌드 시간 단축 - Gradle의 병렬성을 이끌어내고 빌드시간을 단축할 수 있다.
동적 딜리버리 - 모듈화는 앱의 특정 기능들이 조건적으로 혹은 요구사항에 맞게 다운로드 될 수 있도록 허락해주는 Play Feature Delivery의 요구사항이기도 하다.
재사용성 - 적절한 모듈화는 각기 다른 플랫폼에서 코드를 공유하고 여러 개의 앱들을 빌드하는 것을 가능하게 해준다.
그러나, 모듈화는 잘못 사용될 여지가 있고, 앱을 모듈화할 때 몇가지 주의해야 할 gotcha들이 있다!
gotchas? 이게 뭐지!? (처음보는 영어단어다)
찾아보니, gotcha는 문서화된 대로 작동하지만 직관적이지 않고 호출하기 쉽고 결과가 예기치 않거나 비합리적이기 때문에 거의 실수를 불러일으키는 시스템을 뜻한다!
너무 많은 모듈 - 각각의 모듈은 빌드 구성의 복잡성이 증가하는 형태로 발생하는 오버헤드를 갖는다. 이로 인해 Gradle의 sync 시간이 늘어나고, 지속적인 유지 관리 비용이 발생할 수 있다. 또한, 더 많은 모듈을 추가하면, 단일 모놀리틱 모듈과 비교했을 때에 비해 프로젝트의 Gradle 셋업이 복잡해진다. 그런데, 이는 재사용 가능하고 구성 가능한 빌드 구성을 type-safe한 Kotlin 코드로 추출하기 위해 convetion plugin을 사용하여 완화할 수 있다. 해당 프로젝트(now in android)에서는 이러한 convention plugins를 build-logic 폴더에서 찾아 볼 수 있다.
충분하지 않은 모듈 - 반대로 모듈이 적고, 이 모듈들이 서로 크고 밀접하게 의존하고 있다면, 또 다른 모놀리스가 된다. 이것은 모듈화의 장점을 잃어버리게 된다는 뜻이다. 만약 너의 모듈이 너무 크고, 잘 정의된 하나의 single 모듈이 없다면, 모듈을 분할하는 것을 고려해야 한다.
너무 복잡함 - 사실 여기에는 뚜렷한 묘책이 없다. 사실 당신의 프로젝트를 모듈화하는 것이 항상 이치에 맞는 것은 아니다. 주된 요인은 코드베이스의 크기와 상대적인 복잡성이다. 만약 당신의 프로젝트가 특정 임계값 이상으로 성장하지 않을 것으로 예상될 경우에는, 모듈화가 가지는 확장성 및 빌드 시간 향상의 장점을 누리지 못할 것이다.
모든 프로젝트에 적합한 단일 모듈화 전략은 없다는 점에 유의해야 한다. 그러나, 모듈화 전략의 장점을 최대화하고, 단점을 최소화할 수 있는 일반적인 가이드라인이 존재한다.
barebone 모듈은 단순히 Gradle 빌드 스크립트가 있는 디렉토리이다. 그러나 일반적으로 모듈은 하나 혹은 그 이상의 source sets와 resources나 assets의 모음으로 구성된다. 모듈은 독립적으로 빌드되고, 테스트될 수 있다. Gradle의 유연성으로 인해, 프로젝트를 구성하는 방식에는 제약사항이 거의 없다. 일반적으로, 당신은 low-coupling과 high-cohesion을 지키기 위해 노력해야 한다.
Low coupling - 모듈은 한 모듈에 대한 변경 사항이 다른 모듈에 미치는 영향이 거의 없거나 최소화되도록 가능한 한 독립적이여야 한다. 그들은 다른 모듈의 내부 프로세스에 대한 어떠한 지식을 갖고 있어서는 안된다.
High cohesion - 모듈은 하나의 시스템 역할을 하는 코드 모음으로 구성되어야 한다. 명확하게 정의된 책임이 있어야 하며, 특정 도메인 바운더리 내에 있어야 한다. 예를 들어 now in android 프로젝트의 core:network 모듈은 오로지 네트워크 요청, remote data source의 응답 처리, 그리고 다른 모듈에 데이터를 제공하는 역할을 담당한다.
꿀팁 : 위에서 보이는 그림처럼 모듈 그래프는 모듈 간의 의존성을 시각화하기 위한 프로젝트 모듈화 과정 계획 중에 굉장히 유용할 수 있다.
Now in Android 앱에는 다음 타입의 모듈들이 포함되어 있다.
app
모듈 - app
모듈은 앱 레벨과 나머지 코드베이스들을 바인딩하는 scaffolding 클래스들을 포함하며, 예를 들어 MainActivity
, NiaApp
그리고 앱 레벨의 제어 탐색(navigation)등이 있다. 이에 대한 좋은 예는 NiaNavHost
를 통한 navigation 셋팅과, NiaTopLevelNavigation
을 통한 bottom navigation bar의 셋업 과정이 될 수 있다. app
모듈은 모든 feature
모듈들과 필요한 core
모듈들에 의존한다.
feature:
모듈들 - feature 모듈들은 앱에서 단일 책임을 처리하도록 범위가 지정된 기능에 특화된 모듈들이다. 이러한 모듈들은 필요에 따라 테스트를 포함한 다른 앱에서 재사용될 수 있으며, 그러는 동안 여전히 분리되고 독립된 상태를 유지할 수 있다. 만약 하나의 클래스가 하나의 feature
모듈에서만 필요한 경우, 그 클래스는 해당 모듈에 있어야 한다. 그렇지 않다면, 그 클래스는 적절한 core
모듈로 추출되어 사용되어야 한다. feature
모듈은 다른 feature
모듈들에 의존성을 가지면 안된다. feature
모듈들은 그들이 필요로 하는 core
모듈들에만 의존해야 한다.
core:
모듈들 - core:
모듈들은 앱의 다른 모듈들 간에 공유해야 하는 보조적인 코드와 특정 의존성을 포함하는 공통 라이브러리 모듈이다. 이러한 모듈들은 다른 core
모듈들에 의존할 수 있지만, feature:
모듈들이나, app
모듈에는 의존하면 안된다.
기타 모듈들 - sync
, benchmark
, test
그리고 디자인 시스템을 빠르게 보여주는 카탈로그 앱인 app-nia-catalog
와 같은 모듈들도 있다.