일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Codeforces
- android
- AWS
- pandas
- Hilt
- Coroutines
- boj
- 프로그래머스
- livedata
- Compose
- textfield
- GitHub
- TEST
- 암호학
- relay
- 쿠링
- architecture
- 코루틴
- Coroutine
- MyVoca
- 코드포스
- Rxjava
- ProGuard
- activity
- 백준
- Gradle
- androidStudio
- Kotlin
- MiTweet
- Python
- Today
- Total
이동식 저장소
MAU 세 자릿수 서비스 모듈화한 썰 푼다 본문
이전 글에서는 쿠링을 ``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를 사용하면 앱 전체에서 동일한 의존성 버전을 사용할 수 있으며, 의존성 선언 자체도 매우 쉬워진다.
쿠링 앱에서는 ``.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 기능을 사용하면 테스트 코드끼리만 공유되는 코드를 작성할 수 있다.
그런데 Test Fixtures에는 Java 코드만 작성할 수 있고, Kotlin 코드는 인식하지 못한다;; 원래 AGP 8.3.0에서 Kotlin 코드도 지원될 예정이었는데, 8.4.0으로 또 밀린 모양. 언젠가 꼭 지원해 주길...
결과
빌드 시간 비교
모듈화의 목적 중 하나였던 빌드 시간을 비교해 보자. 먼저 전체 빌드 시간을 측정해 보았다.
모듈화 후의 빌드 시간이 더 길었다. 모듈 수에 비례하는 오버헤드가 있기 때문에 어느 정도 예상하긴 했다.
그렇다면 이번엔 코드를 일부만 수정한 후 빌드해 보자. 구체적으로는 4개의 서로 다른 Dao에 쿼리를 하나씩 추가하고, 앱을 실행하는 방식으로 테스트했다.
놀랍게도 모듈화 후의 빌드 시간이 더 길었다. 모듈화로 단축된 빌드 시간 대비 오버헤드가 더 크다고 볼 수도 있지만, 실망스러운 결과다.
그치만
코드의 결합도를 줄이는 데에는 성공했다. 이전 코드는 상호 의존성이 심각하게 높아서, 의존관계를 끊는 모듈화 사전 작업이 모듈화보다 더 오래 걸렸을 정도였다.
다행히 이번 모듈화를 통해 코드의 결합도를 낮출 수 있었고, 새로운 inverse of control 방법도 배울 수 있었다. 앞으로도 더 깔끔한 코드를 작성하기 위해 노력해야겠다.
'프로젝트 > 쿠링' 카테고리의 다른 글
View에서 Compose로 migrate한 썰 푼다 (1) | 2024.06.02 |
---|---|
Proguard에 또 당한 썰 푼다 (0) | 2024.03.01 |
DAU 세 자릿수 서비스에 신기능 배포한 썰 푼다 (0) | 2023.03.08 |