[Android] Refactoring: Multi Module (멀티모듈화)

ByWindow·2022년 10월 14일
4

Android

목록 보기
10/14
post-thumbnail
post-custom-banner

💡 Module?


Google Developer 공식문서를 참조하면

모듈은 소스파일 및 빌드 설정으로 구성된 모음이며, 이를 통해 프로젝트를 별개의 기능 단위로 분할할 수 있습니다. 안드로이드 프로젝트에는 반드시 하나 이상의 앱 모듈이 존재하며, 그 밖에도 하나 이상의 다른 모듈들이 포함될 수 있습니다. 또한, 하나의 모듈이 다른 모듈을 종속 항목으로 사용할 수 있습니다. 각 모듈을 독립적으로 빌드, 테스트, 디버그 할 수 있습니다.

진행하던 프로젝트 크기가 커지고, 클린아키텍처를 적용함에 있어서 멀티모듈화가 필요해졌습니다.
지금부터는 제가 직접 고민하며 멀티모듈을 적용했던 경험을 적어보겠습니다.

💡 MVVM과 클린아키텍처


MVVMClean Architecture

위의 키워드를 공부할 때 가장 많이 보이는 그림입니다.
저는 프로젝트에 MVVM패턴과 클린아키텍처를 적용하고자 했습니다.

왜 MVVM 패턴을 사용했는가?
기존의 MVP 패턴 대신 MVVM을 적용한 가장 큰 이유는 ViewModel의 재사용입니다.
MVVM 패턴에서는 ViewModel과 View과 1:N 관계이기 때문에 하나의 ViewModel로 여러 View를 업데이트 하는 로직을 설계할 수 있습니다. 이는 MVP 패턴에서 Presenter와 View과 1:1 관계로 존재했던 것과 비교했을 때 많은 양의 코드와 클래스 파일들을 줄일 수 있다는 장점이 있습니다.

왜 클린아키텍처를 적용했는가?
가장 큰 이유는 유지보수를 용이하게 하고, 팀프로젝트에서 다른 분야를 담당하는 팀원들과 같이 코드 리뷰를 진행했을 때도 쉽게 이해하도록 하기 위해서 였습니다.

그럼 이제 저의 프로젝트와 함께 실제로 적용한 내용을 살펴 보겠습니다.

🌈 Multi Module


우선, 전체 프로젝트의 모듈 간 관계를 그림으로 나타내보았습니다.

그림에는 포함시키지 않았지만,
모든 모듈에서 참조할 수 있는 Util, Model 모듈이 따로 존재합니다.

App Module

App 모듈에서는 프로젝트 빌드 전 필요한 초기화 작업이 이루어집니다.
Hilt 라이브러리를 사용하기 때문에 HiltAndroidApp 어노테이션을 붙일 MainApplication 클래스가 있습니다.
또한 Timber 라이브러리를 사용하기 때문에 TimberInitializer 클래스도 존재합니다.
gradle의 defendencies는 아래와 같습니다.

Login Module

스플래시스크린 이후 로그인이 완료된 후, 플랫폼 서비스를 이용할 수 있으므로 Login 기능을 하나의 모듈로 분리했습니다.
모듈 간 의존 관계를 살펴보면, model, repository, common, utils 모듈에 대해 의존성을 가집니다.
또한, 모듈안의 hierarchy는 다른 feature 모듈과 동일하게 구성했습니다.

  • bindings : @bindingAdapter 어노테이션을 통해 ViewBinding 작업을 해주었기 때문에 해당 메서드 코드들이 위치합니다.
  • ui : 리사이클러뷰의 adapter, ViewModel, View 관련 파일들이 위치합니다. 또한 하나의 ui폴더 안에 모든 파일을 위치시키는 것보다는 View별로 폴더로 나누어 만듦으로써 파일 탐색이나 계층구조 파악을 쉽게 할 수 있도록 했습니다.
  • utils : login 모듈에서 필요한 함수들을 위한 패키지입니다. 자주 사용하는 구문을 함수로 만들어 boiler plate를 개선했습니다. 사용자 토큰 상태에 따른 Activity 이동이나 버튼 색깔 변경 같이 중복될 수 있는 코드들을 관리합니다.

Base Module

Feature module과 app module 사이에 하나의 모듈을 추가한 이유는 Navigation 때문입니다.
저는 Jetpack에서 제공하는 Navigation Component를 사용했기 때문에 모듈과 모듈 간 fragment 이동을 구현할 수 있는 NavController가 필요했습니다. 그래서 각 feature module의 nav_graph를 include하여 하나로 그룹화할 수 있는 graph를 생성했습니다.

<!-- common/src/main/res/naviagtion/nav_graph.xml -->

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/nav_graph"
  app:startDestination="@id/graph_home">

  <include app:graph="@navigation/graph_home"/>
  <include app:graph="@navigation/graph_dip"/>
  <include app:graph="@navigation/graph_userhouse"/>
  <include app:graph="@navigation/graph_search"/>
  <include app:graph="@navigation/graph_more"/>

</navigation>

이외에 class 파일은 MainActivity가 존재하는데, 아래와 같은 기능을 합니다.

  • FragmentContainerView를 뷰로 가지고, nav_host를 설정합니다.
  • bottomNavigationView를 생성하고, 필요한 설정을 진행합니다.

Feature Module

bottom navigation에 존재하는 다섯개의 기능별로 모듈화를 했습니다.

  • 홈 : 메인페이지를 담당하며, 매물상세화면으로 연결됩니다.
  • 관심목록 : 사용자가 찜하거나 문의하거나 최근에 열람한 방을 보여줍니다.
  • 검색 : 지도를 통해 매물을 검색하는 화면입니다.
  • 나의 집 : 세입자는 자신의 집 안 에셋을 관리하고, 임대인은 자신이 소유한 매물들을 관리할 수 있는 화면입니다.
  • 더보기 : 회원정보관리, 매물등록을 할 수 있는 화면으로 연결됩니다.

각 모듈 내부의 계층구조는 Login module에서와 동일합니다.

Data Module

data 폴더를 만들고 내부에 아래와 같이 관련 모듈들을 생성했습니다.

api

  • API 호출에 필요한 Service 인터페이스Client 클래스가 있습니다.
  • model 모듈에 대한 의존성을 가지며, 종속성 구성은 implementation이 아닌 api로 선언합니다. api로 선언한 이유는 model 모듈에 수정사항이 있을 시, 이에 의존성을 가지는 모든 모듈을 재빌드하기 위함입니다. (관련자료)

api-builder

  • NetWorkModule object를 가지고 있으며, api에서 정의한 service, client와 함께 retrofit, okHttp 라이브러리르 사용하여 API통신에 필요한 객체를 빌드합니다.
  • api 모듈에 대해 의존성을 가집니다.
  • api-builder 없이 api 모듈이 repository 모듈과 의존관계를 가지면 순환참조가 발생할 위험이 있습니다.

db

  • api와 api-builder가 API를 호출하고 Remote 데이터를 위한 모듈이었다면, db 모듈은 로컬저장소에 데이터를 저장하고 로드하기 위한 모듈입니다.
  • api와 api-builder 모듈에 대해 의존성을 가지지 않습니다.
  • Jetpack의 data store 라이브러리를 사용하기 때문에, 싱글톤으로 해당 인스턴스를 생성해주기 위한 DataStoreModule이 있습니다.

repository

  • Repository 패턴을 적용하기 위한 모듈입니다.
  • repository의 가장 중요한 역할은 백엔드 서버에 데이터를 요청하는 것입니다. 그리고 ViewModel이 Data Source를 모르도록 하는 역할분리를 위해 필요합니다.
  • api-builder에서 정의한 네트워크 모듈을 사용하여 API 호출 함수를 실행합니다.
  • 각 repository 인터페이스마다 impl 클래스를 만들었습니다.
    repository 인터페이스에서 함수를 정의하고 impl 클래스에서 repository 인터페이스를 상속한 후 함수를 override하여 구현합니다.
    클래스 파일이 많아진다는 단점이 존재하지만, 서버에서 데이터를 받아올 때 클라이언트 측에서 필요한대로 데이터를 받아오는 것이 힘들고 요구사항대로 변경이 필요한 경우가 많은데, 그런 재가공 작업을 하기에 용이했습니다.
  • db 모듈, api-builder 모듈 각각에 대해 의존성을 가집니다.

Model

  • API에 요청을 보내거나, 응답을 받을 때 필요한 data class들을 정의한 모듈입니다.
  • data 관련 모듈뿐만 아니라 다른 모듈에서도 데이터 클래스를 사용함으로, data 폴더 밖에 따로 존재합니다.

👀 결론


이번 멀티모듈화 작업을 하며 들었던 생각은 개발은 Give and Take라는 것입니다.
모듈을 기능별로 나누고 의존관계를 설정하는 단계에서 고민하는 시간이 많았고, gradle의 dependencies를 선언할 때도 알 수 없는 에러들을 많아서 해결하는 것이 어려웠습니다.
하지만, 멀티모듈화를 한 후에는 빌드시간도 감소했고, boiler plate 코드도 줄어 더 생산성 있는 작업이 가능했습니다.
그리고 무엇보다 프로젝트를 개발하다보면 drawable파일에 계속 xml파일이 추가되어 관리하기가 힘들고 필요한 파일을 찾을 때도 스트레스 받았었는데 이렇게 기능별로 나누고 각 모듈마다 필요한 drawable 파일을 나누다보니 관리가 매우 편합니다.
=> 결론: 멀티모듈화 하세요!

참고자료


codelab Multi Modularizing Apps
Pokedex

profile
step by step...my devlog
post-custom-banner

0개의 댓글