Android 셀렙샵 앱 모듈 Dagger Hilt 연결과 모듈 테스팅

신규찬
CJ 온스타일 기술 블로그
14 min readSep 22, 2023

안녕하세요.
CJ ENM 커머스 부문에서 Android 앱을 개발하는 신규찬입니다.

Android 모듈(Module) 구조는 앱 개발을 더 효율적으로 만들고 유지 관리 및 협업을 용이하게 합니다. 이로 인해 개발자들은 더 빠르고 안정적인 앱을 제작할 수 있습니다.

셀렙샵 앱 프로젝트는 클린아키텍처로 개발하여 각 계층(Layer)는 모듈로 구성하였고 구성요소의 연결은 HIlt 을 사용하였습니다.

셀렙샵에서 어떻게 모듈(Module)구조를 만들고 어떻게 Hilt로 연결하였는지 소개하겠습니다.

모듈(Module)의 장점

모듈(Module)을 개발하는 것은 Android 앱 개발 프로세스의 여러 측면에서 이점을 제공합니다. Android 모듈의 주요 장점은 다음과 같습니다.

  1. 모듈화 및 재사용 Android 모듈을 사용하면 앱을 더 작은 부분으로 나눌 수 있습니다. 이렇게 하면 코드를 모듈 단위로 분리하고 재사용하기 쉽게 만들 수 있습니다. 여러 프로젝트에서 동일한 모듈을 사용하여 개발 생산성을 향상시킬 수 있습니다.
  2. 팀 작업 용이성 대규모 앱을 개발할 때 여러 개발자가 협업하는 경우가 많습니다. 모듈화된 코드는 팀 내에서 더 쉽게 작업할 수 있으며 각 모듈을 별도로 테스트하고 유지 관리할 수 있습니다.
  3. 방어적 프로그래밍 모듈은 특정 기능 또는 역할을 수행하므로 모듈 내에서 발생하는 문제를 독립적으로 해결할 수 있습니다. 이것은 앱의 안정성을 높이고 버그를 신속하게 수정하는 데 도움이 됩니다.
  4. 빌드 시간 최적화 모듈을 사용하면 전체 앱을 빌드하는 대신 변경된 모듈만 다시 빌드할 수 있습니다. 이는 개발 및 테스트 프로세스를 더 빠르게 만들어줍니다.
  5. 테스트 용이성 각 모듈을 독립적으로 테스트할 수 있으며 이로 인해 버그를 더 빠르게 찾고 수정할 수 있습니다.
  6. 기능 단위 개발 모듈을 사용하면 앱의 특정 기능을 독립적으로 개발하여 새로운 기능을 더 쉽게 추가할 수 있습니다.
  7. 버전 관리 모듈을 사용하면 각 모듈을 독립적으로 버전 관리할 수 있으며, 더 쉽게 업데이트 및 유지 관리할 수 있습니다.
셀렙샵 모듈 구조

셀렙샵 Android 앱은 클린아키텍처의 계층(Layer) 별 모듈로 구성하여 앱 개발을 더 효율적으로 만들고 유지 관리 및 협업을 용이하게 하였습니다. 이로 인해 더 빠르고 안정적인 앱을 제작할 수 있었습니다.

Hilt란 무엇인가?

클린 아키텍처는 의존성을 최소화 하여 유연한 소프트웨어로 개발 하는데 목적을 두고 있습니다. 안드로이드 앱 개발에서 구조와 유지 보수성을 고려할 때, 클린 아키텍처와 Hilt를 함께 사용하는 것도 효율적인 앱을 개발하는 방법 중 하나 입니다.

Hilt는 Google에서 개발한 도구로, Dagger 2를 기반으로 하여 Android 앱에서 Dagger를 더 쉽게 활용할 수 있게 해줍니다. Hilt는 안드로이드 앱의 모든 클래스에 의존성 주입 컨테이너를 제공하고, 수명 주기 관리를 자동화하여 앱에서 의존성 주입을 편리하게 사용할 수 있게 합니다.

HIlt 수명 주기 (https://developer.android.com/training/testing/fundamentals)

Hilt 의 구성요소

Hilt의 구성 요소는 계층 구조를 가지고 있으며, 구성 요소에 모듈을 설치하면 해당 구성 요소의 다른 연결 또는 하위 계층 구조에서 그 아래에 있는 하위 구성 요소의 다른 연결 항목으로 설치된 모듈의 연결에 Access 할 수 있습니다.

https://developer.android.com/training/dependency-injection/hilt-android?hl=ko

셀렙샵 앱에서의 모든 API 통신이 Activity Lifecycle에 의존하여 통신이 이뤄지기에, Module의 설치 위치를 ActivityRetainedComponent로 설정하여 Activity의 생명주기(LifeCycle)에 맞게 사용하도록 구성하였습니다.

@Module
@InstallIn(ActivityRetainedComponent::class)
class BestItemRepositoryModule {
@Provides
fun provideBestItemRepository(
remote: BestItemRemoteDataSource
): BestItemRepository = BestItemRepositoryImpl(remote)
}

Hilt을 사용한 클린 아키텍처 구성

셀렙샵 앱에 Hilt을 사용한 클린 아키텍처 구성하는 것은 각 계층(Layer)에서 필요한 의존성을 Hilt를 사용하여 주입하는 것으로 구성을 완료했습니다.

클린아키텍처의 각 계층의 의존을 그림으로 표현하면 다음과 같습니다.

클린아키텍처 계층(Layer) 의존

셀렙샵에서 클린 아키텍처의 각 계층은 Android 모듈(Module)로 구성하였으며, Presentation과 Data 모듈은 Domain을 의존하도록 build.gradle에서 설정하였습니다.

// Data Module build.gradle
dependencies {
implementation project(":domain")
}
// App build.gradle
dependencies {
implementation project(":domain")
}
셀렙샵 아키텍처 구조도

‘셀렙샵은 왜 클린아키텍처를 도입하였는가?’ 의 “클린 아키텍처 구성요소 연결” 에서 소개했던 BestItemRepository 예시로 클린 아키텍처 구성요소 연결 했던 경험을 공유드리면 다음과 같습니다.

클린 아키텍처에서 Repository의 주요 역할은 Domain 계층(Layer)과 Data 계층 간의 데이터 액세스(Access)를 추상화하고, 상호 작용을 관리하여 시스템을 더 견고하고 유지보수 가능하게 만드는 것입니다. BestItemRepository는 의존성을 낮추기 위해 인터페이스로 정의하였습니다.

// Domain Layer
interface BestItemRepository {
suspend fun getBestItemItems(): Result<BestItemEntity>
}

이런 설계 방식을 통해 Domain 계층은 데이터 액세스(Access)의 구체적인 내용을 알지 않고, 단순히 Repository 인터페이스를 통해 데이터를 요청하거나 저장할 수 있습니다.

BestItemRepository 구현체의 constructor 에 @inject annotation을 선언하고 DI(의존성 주입) 하고자 하려는 BestItemRemoteDataSource constructor 의 Param으로 설정합니다.

// Dada Layer
class BestItemRepositoryImpl @Inject constructor(
private val remote: BestItemRemoteDataSource
) : BestItemRepository {
override suspend fun getBestItems(): Result<BestItemEntity> {
val response = remote.getBestItemItems()
// todo.. response에 대한 처리 로직 추가
}
}

BestItemRepository 의 DI component 정의 및 @provides 설정은 Data 계층(Layer)에서 설정합니다. 앞써 공유드렸듯이 DI Component는 Activity의 Lifecycle 에 맞춰 ActivityRetainedComponent 로 설정하였습니다.

@Module
@InstallIn(ActivityRetainedComponent::class)
class BestItemRepositoryModule {
@Provides
fun provideBestItemRepository(
remote: BestItemRemoteDataSource
): BestItemRepository = BestItemRepositoryImpl(remote)
}

이처럼 클린 아키텍처를 구성할 때, 각 구성 요소 간의 의존성을 최소화하고 모듈(Module) 간의 분리를 강화하기 위해 인터페이스(interface)를 사용하여 연결합니다. Repository를 제외한 클린 아키텍처의 각 구성 요소는 그림에서 표시된 대로 독립된 계층 내에서 구성됩니다.

클린아키텍처 구성요소 선언 위치

모듈 테스팅

저희 팀에서는 셀렙샵 앱 개발하며 모듈의 많은 이점을 경험하였습니다. 모듈화 및 재사용과 함께 빌드 시간 최적화를 경험했으며 무엇보다 모듈 테스팅을 통해 안정성을 높였습니다.

https://developer.android.com/training/testing/fundamentals

셀렙샵 앱 유닛테스트(Unit Test)는 JUnit 라이브러리를 활용해서 진행하였습니다. 이를 통해 모든 비즈니스 로직의 안정성을 높였으며 이로 인해 코드를 개선하고 테스트 결과를 코드에 반영했습니다. 무엇보다 클린아키텍처의 각 계층(Layer)를 모듈로 구성하여 각 계층 별로 유닛테스트 시나리오를 구성 및 테스트를 진행할 수 있었습니다.

클린아키텍처 각 계층(Layer) 유닛 테스트(Unit Test)

Android Studio 의 Test는 UI Test 할 수 있는 androidTest 와 unit Test 을 할 수 있는 unitTest 패키지가 존재하며 기본적인 테스트 할 수 있는 샘플(Sample) 코드를 제공하고 있습니다.

Android Studio Test

제공된 샘플(Sample) 유닛 테스트 코드인 ExampleUnitTest.class에 직접 유닛 테스트 시나리오를 작성하거나, 해당 위치에 새로운 클래스를 생성하여 유닛 테스트 시나리오를 작성할 수 있습니다.

그 외 방법으로는 테스트 할 Class 에서 (Mac 기준) Control + Enter 키 입력 후 노출된 Generate 팝업의 “Test” 라는 항목을 클릭하여 유닛 테스트 할 Function을 선택 후 생성 할 수 있습니다.

Generate 을 활용한 유닛 테스트(Unit Test) 생성

Android 셀렙샵 App 프로젝트는 비즈니스 로직을 Unit Test로 App 안정성을 높였습니다.

유닛 테스트를 시작하기 위해 가장 먼저 한 작업 중 하나는 모의 객체(Mock)를 설정하는 것이었습니다. JUnit에서는 모의 객체를 설정하는 작업을 간편하게 수행할 수 있도록, @Before annotation 사용하여 필요한 테스트에 모의 객체를 Mockito를 통해 Mocking할 수 있도록 제공하고 있습니다. setThousandsSeparator Function 에서는 ItemInfoEntitiy 라는 모의 객체(Mock)을 생성하였습니다.

유닛 테스트를 진행할 함수는 @Test annotation 사용하여 선언합니다. 그리고 테스트 결과는 JUnit API의 Assert를 활용하여 예상한 결과값이 올바르게 반환되는지 확인할 수 있었습니다.

JUnit 에서 제공하는 Asser 종류는 다음과 같습니다.

assertEquals(expected, actual)
예상 결과(expected)와 실제 결과(actual)를 비교하여 두 값이 동일한 경우 테스트 성공

assertNotEquals(expected, actual)
예상 결과와 실제 결과를 비교, 두 값이 다른 경우 테스트 성공

assertTrue(condition)
조건(condition)이 참(true)인지 확인, 조건이 참일 경우 테스트 성공

assertFalse(condition)
조건이 거짓(false)인지 확인, 조건이 거짓일 경우 테스트 성공

assertNull(object)
주어진 객체가 null인지 확인, 객체가 null인 경우 테스트 성공

assertNotNull(object)
주어진 객체가 null이 아닌지 확인, 객체가 null이 아닌 경우 테스트 성공

assertSame(expected, actual)
두 객체가 동일한 객체인지 확인(참조 비교), 두 객체가 서로 다른 경우 테스트 성공

assertNotSame(expected, actual)
두 객체가 서로 다른 객체인지 확인(참조 비교), 두 객체가 서로 다른 경우 테스트 성공

fail(message)
테스트를 직접 실패로 표시하고 메시지를 출력

셀렙샵 Android App 에서 사용한 천 단위마다 쉼표(,) 을 붙이는 setThousandsSeparator Function 에서는 JUnit Asser 의 Assert.asserEqulas 를 활용하여 사용하였습니다.

class ProductViewUnitTest {
lateinit var itemInfoEntity: ItemInfoEntity

@BeforeClass
fun setUp() {
itemInfoEntity = Mockito.mock(ItemInfoEntity::class.java)
Mockito.`when`(itemInfoEntity.price).thenReturn(10000000)
}

@Test
fun `천 단위 표시 확인`() {
val price = ConvertUtil.setThousandsSeparator(itemInfoEntity.price)
Assert.assertEquals(true, price?.equals("10,000,000") == true)
}
}

소개한 사례는 Presentation 계층(Layer)의 비즈니스 로직에 대한 유닛 테스트(Unit Test) 였습니다. 또한, 셀렙샵 앱에서는 필요에 따라 Domain 및 Data 계층의 비즈니스 로직에 대한 유닛 테스트 시니리오도 작성하여 테스트 진행하였습니다.

결론

모듈 구조의 장점을 소개하며 셀렙샵 프로젝트 구조를 모듈로 왜 만들었는지 소개했으며, 클린 아키텍처 구성요소를 Hilt로 연결하는 방법을 샘플 코드로 소개하였습니다. 끝으로 모듈 테스팅을 활용한 안정화 사례를 소개 하였는데요. 소개한 내용은 일부분으로 실제 프로젝트 구현할 때는 많은 고민이 필요할 것입니다.

좋은 코드란 복잡한 요구 사항을 심플하게 개발하는 것이 아닐까 생각됩니다. 복잡한 프로젝트도 클린아키텍처를 활용한 프로젝트 모듈 구조로 구성한다면 복잡한 구조를 보다 심플하게 개발하는데 도움이 될 것입니다.

--

--