본문 바로가기

[Android] 기존 코드를 Hilt로 마이그레이션 해보자

@hyeon.s2023. 8. 25. 15:42

들어가기 전
‘엄빠도어렸다’ 프로젝트 2차 스프린트 중, 기존 코드를 Hilt를 통해 의존성 주입 리팩토링을 진행하였습니다.
그 과정과 의존성 주입으로 인해 변경된 점, 의존성 주입과 Hilt의 장점 등을 포스팅하고자 합니다.

기존 코드와 Hilt의 필요성

class HomeRemoteDataSource {
    private val homeService = ServicePool.homeService
}

2차 스프린트 이전의 코드는 정적 객체에 직접 의존하는 방식으로 작성되어 있었습니다.

이 방식의 문제점은 다음과 같습니다:

의존성 관리의 어려움

HomeRemoteDataSource 클래스는 ServicePool의 정적 객체에 직접 의존합니다.

이로 인해 코드 변경이나 테스트에 유연성이 떨어지고, Mock 객체를 활용한 테스트가 어렵습니다.

확장성 부족

새로운 HomeService 구현체를 도입하려면 ServicePoolHomeRemoteDataSource를 모두 수정해야 합니다. 이는 코드 유지보수에 부담을 줍니다.

따라서 외부에서 객체를 주입받는 방식으로 전환할 필요성을 느꼈고, 이를 위해 Dependency Injection(DI)을 도입하게 되었습니다.


Android에서 Dependency Injection

안드로이드에서 DI를 구현할 수 있는 프레임워크로는 Dagger, Hilt, Koin 등이 있습니다.

이 중 Hilt를 선택한 이유는 다음과 같습니다.

Hilt는 안드로이드의 주요 컴포넌트(Activity, Fragment, ViewModel 등)에 DI를 쉽게 적용할 수 있습니다.

또한 Dagger를 기반으로 하며, 복잡한 설정 없이 어노테이션을 활용해 간결한 방식으로 DI를 구성할 수 있습니다.


Hilt 적용 과정

1. Application 클래스

Hilt를 사용하려면 @HiltAndroidApp 어노테이션을 Application 클래스에 추가해야 합니다.

이는 Hilt에 의존성 주입을 시작하는 지점을 알려줍니다.

@HiltAndroidApp
class MyApplication : Application

2. Activity / Fragment

@AndroidEntryPoint 어노테이션을 추가해 Hilt가 해당 클래스의 의존성을 관리하도록 설정합니다.

@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {
    private val viewModel: HomeViewModel by viewModels()
}
private val viewModel: HomeViewModel by viewModels { ViewModelFactory(this) }
private val viewModel: HomeViewModel by viewModels()

이전에는 ViewModel의 의존성 주입을 직접하기 위해서 ViewModelFactory를 사용해야했습니다.

하지만 DI 적용 시 by viewModels 로 hilt가 제공하는 ViewModel 팩토리를 자동으로 사용하므로 더이상 ViewModelFacotry를 작성할 필요가 없어졌습니다. 이로 인해 리팩토링 전후, viewModel 선언 방식이 더욱 간결해졌습니다.


3. ViewModel

Hilt를 사용하려면 @HiltViewModel 어노테이션을 추가하고, 생성자에 @Inject를 사용해 의존성을 주입받습니다.

class HomeViewModel(private val homeRepositoryImpl: HomeRepositoryImpl) : ViewModel()
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val homeRepository: HomeRepository
) : ViewModel()

기존에는 구현체(HomeRepositoryImpl)에 직접 의존했으나, Hilt 적용 후 인터페이스를 통해 의존성을 주입받도록 변경했습니다. 이로인해 결합도를 낮춰지고 테스트에도 유연해지는 효과를 얻을 수 있습니다.


4. Repository

Hilt를 사용하면서 RepositoryImpl을 인터페이스 기반으로 분리했습니다. ViewModel에서 Repository 인터페이스를 주입받도록 변경했습니다.

하지만 이로인해 알맞은 Impl 주입을 위한 Hilt에게 매핑 정보를 제공하기 위해 Module을 작성했습니다.

@InstallIn(SingletonComponent::class)
@Module
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindHomeRepository(
        homeRepositoryImpl: HomeRepositoryImpl
    ): HomeRepository
}

@Module : 의존성 규칙 정의합니다.

@Binds: 인터페이스와 구현체의 1:1 매핑을 정의합니다.

@Singleton: Singleton 범위로 객체를 관리합니다.


5. Service

Retrofit 서비스 생성도 Module을 통해 주입받도록 변경했습니다.

@Module
@InstallIn(SingletonComponent::class)
object ServiceModule {
    @Singleton
    @Provides
    fun provideHomeService(retrofit: Retrofit): HomeService =
        retrofit.create(HomeService::class.java)
}

6. DataSource

DataSource 클래스에서도 @Inject를 사용해 Service 의존성을 주입받도록 수정했습니다.

class HomeRemoteDataSource {
    private val homeService = ServicePool.homeService
}
class HomeRemoteDataSource @Inject constructor(
    private val homeService: HomeService
)

Hilt 리팩토링 후 장점

유지보수성 향상 : 객체 생성 및 의존성 관리를 Hilt가 담당하므로 코드가 간결해지고 변경에 유연해졌습니다.

결합도 감소 : 인터페이스 기반 설계를 통해 클래스 간의 의존성을 낮추고 확장 가능성을 높였습니다.

코드 간소화 : 기존 팩토리 클래스와 정적 객체의 의존성을 제거하여 선언 방식이 간결해졌습니다.

hyeon.s
@hyeon.s :: 개발로그
목차