프로젝트/쿠링

MAU 세 자릿수 서비스 모듈화한 썰 푼다

해스끼 2023. 12. 17. 21:29

이전 글에서는 쿠링을 ``DAU 세 자릿수 서비스``라고 했는데, MAU가 맞다.


쿠링 안드로이드 팀의 숙원 사업이었던 모듈화를 드디어 완료하였다. 첫 커밋이 9월 14일이었으니 거의 2달 넘게 작업한 셈이다. 이렇게 오래 걸릴 일은 아니었는데, 2학기도 너무나 바쁜 탓에 이제야 마무리하고 말았다. ㅠ

 

심지어 이 글조차 모듈화 완료 1개월 후에 작성하고 있다. 이걸 다 할 수 있을 거라고 생각한 과거의 나 죽어

 

이번 글에서는 모듈화 작업을 되돌아보며, 우리가 고민했던 부분과 작업하기 어려웠던 점 등을 정리해 보겠다.

모듈 구조 만들기

먼저 어떤 모듈이 필요하고, 어떤 코드를 어떤 모듈에 옮겨야 할 지 생각해 보았다. Now in Android와 안드로이드 공식 모듈화 문서를 참고하였다.

 

Now in Android: nowinandroid/docs/ModularizationLearningJourney.md at main · android/nowinandroid (github.com)

Android Developers: Common modularization patterns  |  Android Developers

 

쿠링 앱은 모듈을 크게 ``:app``, ``:feature``, ``:common``, ``:data``로 나누었다.

  • ``:app``: 메인 애플리케이션 모듈이다. 다른 `:feature` 모듈을 연결하는 역할을 한다.
  • ``:feature``: Activity, Composable 등 UI 코드가 포함된 모듈이다. 독립된 화면 단위로 모듈을 분리하였다.
  • ``:common``: 다른 모듈에서 반복적으로 사용되는 코드가 포함된 모듈이다. 유틸이나 analytics 코드 등이 포함된다.
  • ``:data``: 모든 데이터 작업과 비즈니스 로직이 포함된 모듈이다. 로컬 DB와 API 통신 코드가 포함된다.

원래 ``:common`` 모듈은 ``:core``로 하려고 했으나, 선배님께서 하신 유틸 코드를 코어라고 부를 수는 없지 않을까요? 라는 질문에 반박할 수 없어 common으로 이름을 바꾸었다. 그 외에 ``:data``와 ``:feature``에서 데이터 전달 포맷으로 사용할 ``:data:domain`` 모듈을 정의했고, 전달 포맷을 ``도메인 객체``로 부르기로 했다.

 

그 밖에도 core와 data의 관계, 객체별로 local/remote 생성 여부 등 다양한 내용을 논의했다. 초안은 내가 작성했고, 선배님과 함께 수정하는 방향으로 작업했다.

모듈 간 의존성 관리

앱에서 사용되는 라이브러리는 앱 전체에서 같은 버전이 사용되어야 한다. 그런데 ``build.gradle``에 버전을 수기로 입력하는 방식으로는 버전을 하나로 관리하기 매우 어렵다.

 

그래서 Gradle Version Catalog를 적용하기로 했다. Version catalog를 사용하면 앱 전체에서 동일한 의존성 버전을 사용할 수 있으며, 의존성 선언 자체도 매우 쉬워진다.

 

[Gradle] Version Catalog로 라이브러리 관리하기

모듈을 여러 개 작성하다 보면 ``build.gradle`` 파일이 복잡해지곤 한다. 같은 라이브러리를 여러 번 작성하다 보면 오타가 날 수도 있고, 버전 관리가 힘들어지기도 한다. 당장 위의 ``:database`` 모듈

thinking-face.tistory.com

쿠링 앱에서는 ``.toml`` 기반 카탈로그를 사용하고 있다. 복잡한 Compose 의존성도 3줄로 끝낼 수 있다!

// build.gradle (module-level)
implementation platform(libs.compose.bom)
implementation libs.bundles.compose
implementation libs.bundles.compose.interop

// libs.versions.toml
[versions]
compose-bom = '2023.05.00'

compose-bom = { module = 'androidx.compose:compose-bom', version.ref = 'compose-bom' }
compose-foundation = { module = 'androidx.compose.foundation:foundation' }
compose-material = { module = 'androidx.compose.material:material' }
compose-material-icons-core = { module = 'androidx.compose.material:material-icons-core' }
compose-material-icons-extended = { module = 'androidx.compose.material:material-icons-extended' }
compose-ui = { module = 'androidx.compose.ui:ui' }
compose-ui-test = { module = 'androidx.compose.ui:ui-test' }
compose-ui-test-junit4 = { module = 'androidx.compose.ui:ui-test-junit4' }
compose-ui-test-manifest = { module = 'androidx.compose.ui:ui-test-manifest' }
compose-ui-tooling = { module = 'androidx.compose.ui:ui-tooling' }
compose-ui-tooling-preview = { module = 'androidx.compose.ui:ui-tooling-preview' }

화면 간 이동 로직

기존 코드에서는 Activity 이동 로직을 다른 activity에 직접 접근하는 식으로 구현했다. ``NoticeWebActivity``를 무려 4개의 activity에서 참조하고 있었을 정도.

 

그런데 모듈화 후에는 다른 ``:feature`` 모듈에 접근할 수 없게 되므로, 다른 Activity에 직접 접근하지 않고도 이동할 수 있어야 한다.

 

선배님과 논의한 결과, 화면 이동 로직을 담당하는 ``KuringNavigator`` 인터페이스를 만들자는 결론이 나왔다. 인터페이스를 ``:core:ui_util``에 선언하면 모든 ``:feature`` 모듈에서 접근할 수 있다. 인터페이스 구현은 모든 ``:feature`` 모듈에 의존하는 ``:app``에서 하면 된다. ``:app``에서 구현한 인터페이스 구현체를 각 ``:feature`` 모듈에서 Hilt로 주입받으면 다른 모듈에 의존하지 않고도 화면 이동 로직을 수행할 수 있다.

 

전형적인 Inversion of Control 기법이다. 

테스트 의존성?

작업을 계속 하던 중, 새로운 문제가 발생하였다. 테스트 코드를 각 모듈로 옮기는 과정에서, 테스트 코드끼리 공유해야 할 의존성이 생겼기 때문이다.

 

예를 들어 테스트 코드용 도메인 객체를 만드는 코드는 Repository나 ViewModel 테스트 코드에서 널리 활용될 수 있다. 그런데 테스트용 코드를 프로덕션 패키지에 작성하는 건 일반적으로 권장되지 않는다. 어떻게 해야 할까?

 

Gradle Test Fixtures 기능을 사용하면 테스트 코드끼리만 공유되는 코드를 작성할 수 있다.

 

테스트 코드에서만 접근 가능한 의존성 만들기

지난 몇 주간 쿠링 안드로이드 앱을 모듈화하고 있다. 여러 이슈가 있었지만 선배님과 함께 잘 풀어나가고 있다. 그 중에서도 오늘은 Gradle의 기능과 관련된 사례 하나를 소개하려 한다. 앱에서

thinking-face.tistory.com

그런데 Test Fixtures에는 Java 코드만 작성할 수 있고, Kotlin 코드는 인식하지 못한다;; 원래 AGP 8.3.0에서 Kotlin 코드도 지원될 예정이었는데, 8.4.0으로 또 밀린 모양. 언젠가 꼭 지원해 주길...

결과

흠...

 

빌드 시간 비교

모듈화의 목적 중 하나였던 빌드 시간을 비교해 보자. 먼저 전체 빌드 시간을 측정해 보았다.

모듈화 후의 빌드 시간이 더 길었다. 모듈 수에 비례하는 오버헤드가 있기 때문에 어느 정도 예상하긴 했다.

 

그렇다면 이번엔 코드를 일부만 수정한 후 빌드해 보자. 구체적으로는 4개의 서로 다른 Dao에 쿼리를 하나씩 추가하고, 앱을 실행하는 방식으로 테스트했다.

?

놀랍게도 모듈화 후의 빌드 시간이 더 길었다. 모듈화로 단축된 빌드 시간 대비 오버헤드가 더 크다고 볼 수도 있지만, 실망스러운 결과다. 

그치만

코드의 결합도를 줄이는 데에는 성공했다. 이전 코드는 상호 의존성이 심각하게 높아서, 의존관계를 끊는 모듈화 사전 작업이 모듈화보다 더 오래 걸렸을 정도였다.

 

다행히 이번 모듈화를 통해 코드의 결합도를 낮출 수 있었고, 새로운 inverse of control 방법도 배울 수 있었다. 앞으로도 더 깔끔한 코드를 작성하기 위해 노력해야겠다.