안녕하세요 이번 시간에는 안드로이드 아키텍처들 중 요새 연구 중인 아키텍처에 대해서 소개하려고 합니다.
자료도 많이 없고 그냥 제 뇌피셜(?)과 동료의 조언을 통해 구축해봤습니다.
컨셉
각 피처 간의 관심사를 완전 분리해놓는 아키텍처입니다. 즉, 각 화면들이 뭘 하든 관심이 전혀 없습니다. 화면이 이동하면 필요한 feature-bradige 를 Dependency 하여 Interface로 정의한 것대로 호출합니다.
안드로이드 스펙
우선 해당 아키텍처에 대해 스펙을 공유드리면 DI 라이브러리로는 Hilt를 사용했습니다. 나머지는 스펙은 필요 없듯합니다.
장점
어떤 서비스업에서 팀원들이 각각 다른 Feature 들을 가지고 병렬적으로 개발하고 난 이후 브렌치에 머지하면 100000% 소스 충돌이 발생하게 됩니다. 아무리 잘 짠 아키텍처라 할지라도 공통 클래스에서 몇 개 추가하고 나면 소스 충돌은 발생하게 됩니다. 이때 Feature 단위로 모듈화 시키면 해당 이슈는 없어지고, 또한 빌드 러닝 시간이 많이 짧아진다는 장점이 있습니다.
- 소스 충돌 Zero
- 빌드 시간 축소
단점
해당 아키텍처로 개발하면 보통 화면 단위로 모듈화를 하는데 new module 시키고 설정 같은 거 처리하는 게 매~~~우 귀찮습니다.
(하지만, 이 또한 개선할 방법이 있습니다. AKA. bash 스크립트 짜서 자동화)
간단하게 제가 Multi-Features 아키텍처에 대해서 연구하면서 겪은 장점과 단점을 소개했습니다. 그렇다면 클린 아키텍처와 Multi-Features와 어떤 차이점이 있는지 그림으로 설명해드리겠습니다.
클린 아키텍처
위 사진과 같이 클린 아키텍처로 되어 있다 하면 data, domain, presentation 모듈로 이루어지고 UI 계층 이외 data 계층에 로그인 토큰이라던지 UI 계층에서 필요한 Util 클래스들을 모듈화 한 "shared" 이렇게 모듈들이 구성되어 있습니다. (app 모듈은 스킵.)
클린 아키텍처 관련해서는 자료가 아주 방대하기 때문에 각 모듈별로 설명은 따로 안 하겠습니다.
멀티 피처
좀 더 부연 설명을 하겠습니다.
Core (Module) | 모든 Feature 모듈에 Dependecy 해야 하는 모듈입니다. 해당 모듈안에는 데이터를 가져오는 Repository Interface 와 각 피처에 공통으로 필요한 ResourceManager, LoginManager 등등.. 관련된 인터페이스들이 구성되어있습니다. |
Features (Directory) | 단순 피처들이 있는 디렉토리 입니다. 그냥 폴더입니다. |
feature1 (Module) | 피처 모듈입니다. ex.) 로그인 피처, 상세 피처 등등.. |
feature1-bridge (Module) | feature1 를 다른 피처에서 사용하고 싶을때 Dependecy 해야 하는 모듈입니다. 다른 화면간 이동할때는 feature 모듈을 직접 Dependecy 하지 않고 해당 모듈과 같이 bridge 모듈을 Dependecy 하여 간적접적으로 접근합니다. 해당 모듈에는 feature1 으로 이동할때 필요한 매개변수관련하여 interface 구성되어 있습니다. |
Network, Shared... | 각 피처에서 사용할 네트워크, ThirdParty UtilClass 관련하여 구현하는 소스 모듈입니다. core 모듈에서 Interface 화 한걸 가지고 여기서 Impl 합니다. |
좀더 자세하게 Android Project 단위로 보여드리면 아래와 같습니다.
클린 아키텍처 (AS-IS) | Multi-Features |
큰 틀을 보자면 클린 아키텍처는 data, domain, presentation 모듈 기반으로 구성되어 있고, 반면에
Multi-Features 같은 경우 core, feature, feature-bridge로 구성되어 있습니다.
여기까지 클린 아키텍처와 멀티 피처들을 비교하면서 차이점을 보여드렸고, 이제 어떻게 구현을 했는지 알려드리겠습니다.
1. 각 피처(화면) 간의 이동 처리
이 아키텍처의 정의는 화면 간의 관심사 완전 분리에 맞게 feature 들을 직접적으로 Dependency 하면 안 됩니다.
그 이유는 아래와 같습니다.
예를 들어 상품 상세(feature)와 상품 목록(feature) 이렇게 구조가 되어 있습니다. 이때 상품 목록에서 상품 상세페이지를 진입하기 위해서 상품 목록 build.gradle 에 상품 상세 모듈을 직접적으로 Dependency 하면 상품 상세에서의 상품 데이터 모델과 상품 목록에서의 상품 데이터 모델 간 네이밍이 같으면 안 되고 서로의 UseCase 나 이러한 것들이 네이밍이 같다면 충돌이 일어날 수 있습니다.
피처 단위로 관심사 완전 분리가 원활히 되지 않기 때문에 직접적으로 Dependency 하면 안 됩니다.
해결법
저 같은 경우에는 각 피처(feature) 마다 bridge라는 모듈을 추가로 만들어서 해결했습니다. 해당 모듈에는 아래와 같이 해당 피처를 이동 처리하는 Interface 만 선언해놓습니다.
interface BaseMvvmBridge {
fun moveToBaseMvvm()
}
그리고 해당 feature 모듈에는 bridge 모듈을 Dependency 하여 해당 Interface의 구현체를 처리합니다.
internal class BaseMvvmBridgeImpl @Inject constructor(
@ApplicationContext private val context: Context
) : BaseMvvmBridge {
override fun moveToBaseMvvm() {
Intent(context, RefactorBaseTestActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(this)
}
}
}
이후 각 피처마다 해당 피처를 사용하고 싶다면 feature-bridge 모듈을 Dependency 하여 간접적으로 의존할 수 있게끔 처리하면서 이 고민 사항을 처리했습니다.
한마디로 클린 아키텍처에서 data -> domain <- presentation 관계라고 보면 될 거 같습니다 :)
2. UseCase 패턴은 어디에 두어야 하나..?
클린 아키텍처를 실제로 사용하다 보면 UseCase 패턴에 대해 득을 많이 보고 "아..이래서 UseCase 패턴을 사용하는 구나" 를 알게 됩니다. 데이터를 로컬 / 서버에서 가져와서 UI로 가기직전 비즈니스 로직을 UseCase에서 처리해버리면 꽤나 장점들이 많습니다.
그래서 멀티 피처 모듈에서는 UseCase 패턴을 어디에 녹일까 고민해보다가 각 피처에서 처리하는 게 낫겠다 싶어 "각 피처에서 처리하자"로 결정했습니다. 그 이유는 아래와 같습니다.
1. 관심사 완전 분리에 맞게 각 피처마다 UseCase 들을 처리해야 합니다. 물론 비슷한 API 경우 UseCase를 계속해서 만든다는 단점이 있지만, 이 아키텍처의 컨셉에 맞게 각 피처마다 따로따로 UseCase 관리하고 처리해야 합니다.
그렇기 때문에 UseCase 처리는 각 피처 안에서 처리하는 걸로 결정했습니다.
3. 테스트 환경 구성
서비스가 점점 고도화가 되면 될수록 테스트 환경 구성하는 게 제일 중요합니다. 저 같은 경우에는 각 피처마다 테스트 환경을 구성하고 싶었지만, 잘 안돼서 test 모듈을 하나 만들어서 아래와 같이 처리했습니다.
// app Application Module
build.gradle
dependencies {
...
androidTestImplementation(project(":test"))
}
// test Module
buid.gradle
dependencies {
...
androidTestImplementation(project(":network"))
androidTestImplementation(project(":core"))
androidTestImplementation(project(":features:base-mvvm"))
androidTestImplementation(project(":features:base-mvvm-bridge"))
androidTestImplementation(project(":features:main"))
androidTestImplementation(project(":features:network"))
androidTestImplementation(project(":features:network-bridge"))
androidTestImplementation(project(":features:recyclerview"))
androidTestImplementation(project(":features:recyclerview-bridge"))
}
Application Module에서 테스트 모드에만 test 모듈이 Dependency 되도록 처리했습니다.
4. 남은 과제들..
글로는 이렇게 간단하게 표현했지만, 여러 수많은 문제와 고민들이 있었습니다. 퇴근 후 계속 고민하면서 어떻게 하면 좀 더 콘셉트에 맞게 설계를 할까..라는 생각을 가지면서 Multi-Features라는 것을 연구했습니다만, 아직 남은 과제들이 몇 개가 있고, 그러한 과제는 아래와 같습니다.
1. 피처 모듈 생성 자동화
각 화면 단위로 모듈을 생성하다 보니 개발자는 new module에 피로도가 많이 쌓이고 기본적으로 Dependency 해야 하는 단순 노가다에 피로감을 느낍니다. 그래서 아는 지인분의 아이디어인 "원하는 디렉토리에 피처 모듈 생성 및 여러 기본적인 클래스 및 파일들을 생성" 해주는 스크립트를 작성해서 개발자 피로도를 줄입니다.
2. 각 피처 단위로 테스트 환경 구축
이 또한 아는 지인분의 아이디어인데 각 피처 단위에 맞게 프로젝트를 자동으로 만들어서 해당 피처만 테스트를 진행할 수 있는 스크립트 문을 작성합니다.
5. 마무리..
이로서 멀티 피처에 대한 포스팅은 끝났습니다. 해당 아키텍처를 연구할 때 자료가 너~어어무 없었습니다. 분명 서비스 기업들도 사용할 텐데... 아직은 알려지지 않은 영역이라 제가 설계한 구조가 맞는지도 의문이 들었습니다. 클린 아키텍처도 정답이 없듯이 이것도 정답은 없는 거 같다는 생각이 들었습니다.
소프트웨어에 맞게 유들유들하게 업무 환경에 맞게 사용하는 것이 맞는 거 같습니다.
해당 연구한 자료는 제 TIL에 설계 구조에 되어 있습니다.
https://github.com/sieunju/TIL/tree/develop/android
긴 글 읽어주셔서 고맙습니다.
'android' 카테고리의 다른 글
[안드로이드] Fastlane & Google Play 배포 환경 구축 해보기 (1) | 2022.10.03 |
---|---|
[안드로이드] 앱 테스터에 자동 배포 도전해보기! (1) | 2022.09.29 |
[안드로이드] ReactiveX 개념 소개 (1) | 2022.07.03 |
[안드로이드] LiveData, MutableLiveData 사용법 (2) | 2022.06.26 |
[안드로이드] 딥링크 테스트 어플 소개 (0) | 2022.06.05 |