[Android] 모듈화

성승모·2025년 9월 26일

Android

목록 보기
2/8

개요

배경

 프로젝트가 커질수록 확장성, 가독성 및 전반적인 코드 품질이 떨어지는 겨웅가 많다. 그 이유는 규모가 커짐에도 유지 관리할 수 있는 구조를 적용하기 위한 노력을 하지 않았기 때문이다. 이 유지 관리할 수 있는 구조 중 하나가 모듈화란 기술이다.

모듈화란?

 코드 내 느슨하게 결합된 부분들을 독립적인 부분으로 따로 빼서 구성하는 방법이다. 각 모듈은 명확한 역할을 하며, 큰 문제를 작은 문제 나누기 때문에 대형 시스템 설계 및 유지보수의 복잡성이 줄어든다.

모듈화의 이점

  • 재사용성: 코드를 공유하고 동일한 기반으로 여러 앱을 빌드할 수 있다.
  • 엄격한 공개 상태 제어: 외부에 노출할 내용을 쉽게 제어가 가능하다.
  • 맞춤설정 가능한 전송: 앱 번들 특성을 활용해 각 모듈을 따로 배포할 수 있다.
  • 확장성, 소유권, 캡슐화, 빌드 시간, 테스트 가능성

모듈화 이전에 생각해볼 것

 모듈로 구성된 정도를 나타내는 말로 '세분화'가 있다. 세분화되면 될수록 모듈이 작고 수가 늘어난다. 따라서, 설계 때 다음 문제를 고려해서 세분화 수준을 결정해야 한다.

  • 너무 세분화됨: 빌드 복잡성 및 상용구 코드 증가로 오버헤드 발생
  • 너무 대략적임: 모듈이 너무 커 모듈성이 제공하는 이점을 갖기 어려움
  • 너무 복잡함: 확장 가능성 및 빌드 시간이 크지 않다면 모듈화를 안 해도 됨.

모듈화 패턴

높은 응집력 및 낮은 결합력 원칙

 당연히 서로 다른 역할로 나누었기 때문에 낮은 결합력을 가지고, 같은 역할을 하는 코드끼리 뭉쳤기 때문에 높은 응집력을 가질 것이다.

모듈 유형

데이터 모듈

데이터 모듈은 모델 클래스, 데이터소스, 저장소로 이루어져 있으면 주로 다음 역할을 수행한다.

  • 모든 데이터 및 비즈니스 로직 캡슐화
  • 저장소를 외부 API로 노출
  • 외부로부터 데이터 소스 숨기기

기능 모듈

UI단 및 이에 밀접하게 관련된 기능을 의미한다. 따라서, viewmodelState과 연결된 가능성이 높다.

기능 모듈은 데이터 모듈에 종속된다.

Play Feature Delivery
: 기능 모듈을 조건부 또는 주문형으로 제공하는 것을 돕는다.

앱 모듈

애플리케이션의 진입점으로 기능모델에 종속된다. 여러 기기를 타겟으로 하는 경우 기기별로 앱 모듈을 정의해주면 된다.

일반 모듈

다른 모듈에서 자주 사용하는 코드가 포함되어 있으며, 중복성을 줄이는 역할을 하며 앱 아키텍처의 특정 레이어가 아닌 모듈이다.

  • UI 모듈
  • 애널리틱스 모듈
  • 네트워크 모듈
  • 유틸리티 모듈

테스트 모듈

테스트를 위한 Android 모듈로, 런타임에는 필요하지 않은 코드, 리소스 등이 포함된다. 다음 역할을 한다.

  • 테스트 코드 공유
  • 깔끔한 빌드 구성
  • 통합 테스트
  • 대규모 애플리케이션 관리

모듈 간 통신

 모듈 간 직접 통신은 무조건 피하고, 결합력을 낮게 유지하여 독립성을 유지해주어야 한다. 이를 위해 "중재 모듈"이 필요하다.

중재자 예시

아래와 같은 모듈들이 존재한다. :core:eventbus이 중재 역할을 할 것이다.

:app                   // 앱 실행 모듈, 의존성 주입/DI 세팅
:core:eventbus         // (중재자) 이벤트 버스 정의
:feature:login         // 기능 로그인
:feature:payment       // 기능 결제

:core:eventbus

// 공통 이벤트 정의
sealed class AppEvent {
    object OpenFeaturePayment : AppEvent()
}
object EventBus {
    private val _events = MutableSharedFlow<AppEvent>()
    val events: SharedFlow<AppEvent> = _events

    suspend fun send(event: AppEvent) {
        _events.emit(event)
    }
}

:feature:login) 모듈 요청

class FeatureLoginViewModel : ViewModel() {
    fun onButtonClicked() {
        viewModelScope.launch {
        	// feature: payment 열어달라고 요청
            EventBus.send(AppEvent.OpenFeaturePayment)
        }
    }
}

:feature:payment) 요청 수신

class FeaturePaymentHandler {
    fun observeEvents() {
        CoroutineScope(Dispatchers.Main).launch {
            EventBus.events.collect { event ->
                when (event) {
                    is AppEvent.OpenFeaturePayment
                    	-> println("Feature Payment 실행!")
                }
            }
        }
    }
}

종속 항목 역전

 추상화와 구현체를 분리하여 코드를 구성하는 것을 의미하며, Database가 필요할 때, Firebase든 Room이든 구현 방식이 상관없다. 데이터를 읽고 쓰기만 가능하면 될 것이다. 따라서, interface Database는 "기능 모듈"에 정의되고, 구현체는 "일반 모듈"에서 구현되면 된다. 추상부와 구현부를 분리함으로써 다음과 같은 이점을 갖는다.

  • 상호 교환성
  • 분리
  • 테스트 가능성
  • 빌드 성능 개선

구현 방법

  1. 추상화 모듈 만들기
  2. 구현 모듈 만들기
  1. 상위 모듈을 추상화 모듈에 종속
  1. 구현 모듈 제공

권장 사항

구성을 일관되게 유지

모듈이 많아질수록 오버헤드는 커질 수 밖에 없고, 사람이 유지 보수하기 어려워진다. 따라서 Gradle의 도구가 이를 도와준다.

가능한 한 노출 최소화

필요한 부분만 노출하도록 하고 가능한 최소 수준으로 공개한다.

Kotlin 및 Java 모듈 선호

Android Stuido에서는 다음 세가지 필수 모듈을 지원한다.

  • 앱 모듈: 소스 코드, 리소스, 애셋 및 AndroidManifest.xml
  • 라이브러리 모듈: 다른 Android 모듈에 종속 항목으로 사용됨.
  • Kotlin 및 Java 모듈: 가능한 Android 모듈 대신 먼저 선택하자.

모듈화 해보기

 현 프로젝트의 모듈화를 진행해보자. 안드로이드 권장 아키텍처을 따라 위와 같은 구조를 가지고 있다. 각 레이어는 다음과 같은 모듈로 정리될 것이다.

모듈 분류

Presentation Layer

일반 모듈의 UI 모듈에 적합하다. utils는 core 모듈에, MainActivity.kt는 앱 모듈에 포함시킨다.

Domain Layer

중재 역할을 한다. 문제가 하나 있는데, result 디렉토리 내 코드다.

sealed class MemberFlowResult {
    data object Loading: MemberFlowResult()
    data class Success(val list: List<Member>): MemberFlowResult()
    data class Fail(val exception: Throwable): MemberFlowResult()
}

위와 같이 UI와 굉장히 밀접한 코드를 갖고 있다. 따라서, Result가 아닌 State로 바꾸고 이를 presentaion/state로 옮겨 ViewModel에서 상태를 emit하는 식으로 리팩토링해야겠다.

Data Layer

 당연히 데이터 모듈에 포함시키며, utils 디렉토리만 core 모듈에 포함시키면 되겠다. 또한 Entity는 Dto나 Model을 따로 만들어, Domain Layer에 넣는 것이 권장된다.

App Layer

Presentation Layer에 있던 MainActivity.kt와 DI 디렉토리를 포함시키면 되겠다.

모듈로 만들기

생성

  • File - New - New Module 생성이 완료되면 settings.gradleinclude(":모듈이름")가 추가된다.
  • :presentation:data는 Android Library로, :domain:core는 kotlin/java Libarary로 생성한다.
  • 기존의 res 디렉토리를 :presentation에 넣어준다.

의존 관계

[:core]는 모든 레이어에서 사용할 수 있고 [:app]은 모든 모듈에 의존한다.
  • build.gradle(Module:app)
  implementation(project(":data"))
  implementation(project(":presentation"))
  implementation(project(":domain"))
  implementation(project(":core"))

  (다른 모듈에서도 의존관계에 따라 추가해준다)

코드 수정

  1. 패키지-디렉토리 경로 맞춰주기
  2. Dto를 추가하고 Entity와 Dto를 서로 바꿀 수 있는 함수 추가
	// :data/entities/Member.kt
    fun Member.toDto(): MemberDto {
        return MemberDto(
            name = this.name,
            birth = this.birth,
            position = this.position,
            joinDate = this.joinDate,
            phoneNumber = this.phoneNumber,
            id = this.id
        )
    }
    fun MemberDto.toEntity(): Member {
        return Member(
            id = id,
            name = name,
            birth = birth,
            position = position,
            joinDate = joinDate,
            phoneNumber = phoneNumber
        )
    }
  1. Flow 수정
  • :data에서는 Flow<Member>처럼 값만 가져오도록 한다.
  • :domain에서 Result를 이용하여 Flow<Result<Member>>를 원한다면 다음과 같은 커스터 Result를 써도 좋을거 같다.
    sealed class Result<out T> {
        data class Success<T>(val data: T) : Result<T>()
        data class Error(val exception: Throwable) : Result<Nothing>()
        data class Loading(val msg: String): Result<T>()
    }
  • :presentation의 ViewModel에서 Flow를 구독하고 State를 바꿔주면 된다.
profile
안녕하세요!

0개의 댓글