일반적인 모듈화 패턴

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

모든 프로젝트에 맞는 하나의 모듈화 전략은 없습니다. Gradle의 유연한 특성으로 인해 프로젝트를 구성하는 방법에는 제약이 거의 없습니다. 이 페이지에서는 다중 모듈 Android 앱을 개발할 때 사용할 수 있는 일반적인 규칙과 공통 패턴을 간략히 설명합니다.

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

모듈식 코드베이스를 특징짓는 한 가지 방법은 결합력응집력 속성을 사용하는 것입니다. 결합력은 모듈이 서로 종속된 정도를 측정합니다. 이 맥락에서 응집력은 단일 모듈의 요소가 기능적으로 관련된 방식을 측정합니다. 일반적으로 결합력은 낮추고 응집력은 높여야 합니다.

  • 결합력이 낮다는 것은 모듈이 최대한 서로 독립적이어야 한다는 의미입니다. 그러면 한 모듈의 변경사항이 다른 모듈에 미치는 영향이 없거나 최소화됩니다. 모듈은 다른 모듈의 내부 작동을 알 수 없어야 합니다.
  • 응집력이 높다는 것은 모듈이 시스템 역할을 하는 코드 모음으로 구성되어야 함을 의미합니다. 모듈은 맡은 일이 명확히 규정되어 있고 특정 도메인 지식의 범위를 벗어나지 않아야 합니다. 샘플 eBook 애플리케이션을 생각해 보세요. 동일한 모듈에 도서와 결제 관련 코드를 함께 사용하는 것은 부적절할 수 있습니다. 두 코드가 서로 다른 두 개의 기능 도메인이기 때문입니다.

모듈 유형

모듈을 구성하는 방법은 주로 앱 아키텍처에 따라 다릅니다. 다음은 권장 앱 아키텍처를 따르면서 앱에 도입할 수 있는 일반적 유형의 모듈입니다.

데이터 모듈

데이터 모듈에는 일반적으로 저장소, 데이터 소스, 모델 클래스가 포함되어 있습니다. 데이터 모듈의 세 가지 주된 역할은 다음과 같습니다.

  1. 특정 도메인의 모든 데이터 및 비즈니스 로직 캡슐화: 각 데이터 모듈은 특정 도메인을 나타내는 데이터를 처리해야 합니다. 관련이 있는 데이터라면 다양한 유형의 데이터를 처리할 수 있습니다.
  2. 저장소를 외부 API로 노출: 데이터 모듈의 공개 API는 데이터를 앱의 나머지 부분에 노출하는 일을 담당하기 때문에 저장소여야 합니다.
  3. 외부로부터 모든 구현 세부정보 및 데이터 소스 숨기기: 데이터 소스는 같은 모듈의 저장소에서만 액세스 가능해야 합니다. 외부에는 공개되지 않습니다. Kotlin의 private 또는 internal 공개 상태 키워드를 사용하여 데이터 소스를 숨길 수 있습니다.
그림 1: 샘플 데이터 모듈과 콘텐츠

기능 모듈

기능은 일반적으로 화면 또는 밀접하게 관련된 일련의 화면(예: 가입 또는 결제 흐름)에 해당하는 독립적인 앱 기능을 의미합니다. 앱에 하단 탐색 메뉴가 있는 경우 각 대상이 기능일 가능성이 높습니다.

그림 2: 이 애플리케이션의 각 탭은 기능으로 정의할 수 있음

기능은 앱의 화면 또는 대상과 연결됩니다. 따라서 로직과 상태를 처리하기 위한 UI와 ViewModel이 연결될 가능성이 높습니다. 단일 기능이 단일 보기나 단일 탐색 대상으로 제한될 필요는 없습니다. 기능 모듈은 데이터 모듈에 종속됩니다.

그림 3: 샘플 기능 모듈 및 콘텐츠

앱 모듈

앱 모듈은 애플리케이션의 진입점입니다. 앱 모듈은 기능 모듈에 종속되며 일반적으로 루트 탐색을 제공합니다. 빌드 변형을 사용하면 단일 앱 모듈을 다양한 바이너리로 컴파일할 수 있습니다.

그림 3: '데모(demo)' 및 '전체(full)' 제품 버전 모듈의 종속 항목 그래프

앱이 자동차, 웨어러블 기기 또는 TV와 같은 여러 기기 유형을 타겟팅하는 경우 기기별로 앱 모듈을 정의하는 것이 좋습니다. 이는 플랫폼별 종속 항목을 구분하는 데 도움이 됩니다.

그림 4: Wear 앱 종속 항목 그래프

일반 모듈

일반 모듈(핵심 모듈이라고도 함)에는 다른 모듈에서 자주 사용하는 코드가 포함됩니다. 일반 모듈은 중복성을 줄이는 역할을 하며, 앱 아키텍처의 특정 레이어를 나타내지는 않습니다. 다음은 일반적인 모듈의 예입니다.

  • UI 모듈: 앱에서 맞춤 UI 요소를 사용하거나 정교한 브랜딩을 사용하는 경우 모든 기능을 재사용할 수 있도록 위젯 컬렉션을 하나의 모듈로 캡슐화하는 것이 좋습니다. 이렇게 하면 서로 다른 기능에서 UI를 일관되게 만들 수 있습니다. 예를 들어 테마 설정이 일원화되어 있다면 리브랜딩이 발생할 때 골치 아픈 리팩터링 작업을 피할 수 있습니다.
  • 애널리틱스 모듈: 일반적으로 추적은 소프트웨어 아키텍처에 대한 고려 없이 비즈니스 요구사항에 따라 정해집니다. 애널리틱스 추적기를 서로 관련 없는 여러 구성요소에 사용하는 경우가 많으며, 이 경우 전용 애널리틱스 모듈을 사용하는 것이 좋습니다.
  • 네트워크 모듈: 많은 모듈에 네트워크 연결이 필요한 경우 http 클라이언트 제공 전용 모듈을 사용하는 것이 좋습니다. 이는 클라이언트에 맞춤 구성이 필요할 때 특히 유용합니다.
  • 유틸리티 모듈: 도우미라고도 하는 유틸리티는 일반적으로 애플리케이션 전체에서 재사용되는 작은 코드입니다. 유틸리티의 예로는 테스트 도우미, 통화 형식 지정 함수, 이메일 검사기 또는 맞춤 연산자가 있습니다.

모듈 간 통신

모듈은 완전히 분리된 경우는 거의 없으며 다른 모듈에 의존해 서로 통신하는 경우가 많습니다. 모듈이 함께 작동하고 정보를 자주 교환하는 경우에도 결합력을 낮게 유지하는 것이 중요합니다. 경우에 따라 두 가지 모듈 간의 직접 통신은 아키텍처 제약 조건의 경우에서처럼 바람직하지 않습니다. 두 가지 모듈 간의 직접 통신은 순환 종속 항목 등으로 인해 불가능할 수도 있습니다.

그림 5: 모듈 간의 직접적인 양방향 통신은 순환 종속 항목 때문에 불가능함. 다른 두 독립 모듈 간의 데이터 흐름을 조정하려면 중재 모듈 필요

이 문제를 극복하기 위해 두 개의 다른 모듈 간을 중재하는 세 번째 모듈을 둘 수 있습니다. 중재 모듈은 두 모듈의 메시지를 수신 대기하고 필요에 따라 메시지를 전달할 수 있습니다. 샘플 앱에서는 이벤트가 다른 기능에 속하는 별도의 화면에서 시작되었더라도 구매할 도서가 결제 화면에 인식되어야 합니다. 이 경우 중재 모듈은 탐색 그래프를 소유한 모듈(일반적으로 앱 모듈)입니다. 이 예에서는 Navigation 구성요소를 사용하여 탐색을 통해 홈 기능의 데이터를 결제 기능으로 전달합니다.

navController.navigate("checkout/$bookId")

결제 대상은 도서 ID를 인수로 수신하고, 이를 도서 정보를 가져오는 데 사용합니다. 개발자는 저장된 상태 핸들을 사용하여 대상 기능의 ViewModel 내부에서 탐색 인수를 검색할 수 있습니다.

class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {

   val uiState: StateFlow<CheckoutUiState> =
      savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
          // produce UI state calling bookRepository.getBook(bookId)
      }
      …
}

객체를 탐색 인수로 전달해서는 안 됩니다. 대신 데이터 영역에서 원하는 리소스에 액세스하고 로드하기 위해 기능에서 사용할 수 있는 간단한 ID를 사용합니다. 이렇게 하면 결합력은 낮게 유지하고 단일 소스 저장소 원칙을 위반하지 않습니다.

아래 예에서 두 기능 모듈은 동일한 데이터 모듈에 종속됩니다. 이렇게 하면 중재자 모듈이 전달해야 하는 데이터 양을 최소화하고 모듈 간의 결합력을 낮게 유지할 수 있습니다. 모듈은 객체를 전달하는 대신 기본 ID를 교환하고 공유 데이터 모듈에서 리소스를 로드해야 합니다.

그림 6: 공유 데이터 모듈을 사용하는 2개의 기능 모듈

일반 권장사항

처음에 언급했듯이 다중 모듈 앱을 개발할 수 있는 하나의 올바른 방법은 없습니다. 많은 소프트웨어 아키텍처가 있는 것처럼 앱을 모듈화하는 방법도 다양합니다. 그렇지만 다음과 같은 일반 권장사항은 코드의 판독과 유지관리 및 테스트 가능성을 높여주는 데 도움이 됩니다.

구성을 일관되게 유지

모든 모듈에는 구성 오버헤드가 발생합니다. 모듈 수가 특정 기준점에 도달하면 일관된 구성을 관리하기가 어렵습니다. 예를 들어 모듈에서 동일한 버전의 종속 항목을 사용하는 것이 중요합니다. 단지 종속 항목 버전을 늘리기 위해 많은 수의 모듈을 업데이트해야 하는 경우 많은 노력이 들어갈 뿐 아니라 실수의 가능성도 발생합니다. 이 문제를 해결하려면 Gradle 도구 중 하나를 사용하여 구성을 중앙 집중화하면 됩니다.

  • 버전 카탈로그는 동기화 중에 Gradle에서 생성된 종속 항목의 유형 안전 목록입니다. 이는 모든 종속 항목을 선언할 수 있는 중앙 위치로, 프로젝트의 모든 모듈에서 사용 가능합니다.
  • 규칙 플러그인을 사용하여 모듈 간에 빌드 로직을 공유합니다.

가능한 한 노출 최소화

모듈의 공개 인터페이스는 최소화하고 필수 부분만 노출해야 합니다. 구현 세부정보가 외부에 유출되면 안 됩니다. 모든 범위를 가능한 한 최소 수준으로 지정합니다. Kotlin의 private 또는 internal 공개 상태 범위를 사용하여 선언을 모듈 비공개로 설정합니다. 모듈에서 종속 항목을 선언할 때는 api보다 implementation을 사용하는 것이 좋습니다. 전자는 모듈 소비자에게 전이 종속 항목을 노출합니다. 구현을 사용하면 다시 빌드해야 하는 모듈의 수가 줄어들기 때문에 빌드 시간을 개선할 수 있습니다.

Kotlin 및 자바 모듈 선호

Android 스튜디오에서 지원하는 세 가지 필수 모듈 유형은 다음과 같습니다.

  • 앱 모듈은 애플리케이션의 진입점입니다. 앱 모듈은 소스 코드, 리소스, 애셋 및 AndroidManifest.xml을 포함할 수 있습니다. 앱 모듈의 출력은 Android App Bundle(AAB) 또는 Android 애플리케이션 패키지(APK)입니다.
  • 라이브러리 모듈에는 앱 모듈과 동일한 콘텐츠가 포함되어 있습니다. 라이브러리 모듈은 다른 Android 모듈에 종속 항목으로 사용됩니다. 라이브러리 모듈의 출력은 앱 모듈과 구조적으로 동일한 Android 보관 파일(AAR)입니다. 하지만 이는 나중에 다른 모듈에서 종속 항목으로 사용할 수 있는 Android 보관 파일(AAR)로 컴파일됩니다. 라이브러리 모듈을 사용하면 여러 앱 모듈 간에 동일한 로직과 리소스를 캡슐화하고 재사용할 수 있습니다.
  • Kotlin 및 자바 라이브러리에는 Android 리소스, 애셋 또는 매니페스트 파일이 포함되지 않습니다.

Android 모듈에는 오버헤드가 발생하므로 가능하면 Kotlin 또는 자바 종류를 사용하는 것이 좋습니다.