본문 바로가기

Android

[Android] 프로젝트에 Hilt로 의존성 주입하기 (다온길 마이그레이션)

 

 

다온길 프로젝트를 진행하며 처음엔 클린 아키텍처의 구조를 이해하는 단계라 Data Layer, Domain Layer, Presentation Layer를 모듈로 분리가 아닌 패키지로만 분리를 해서 코드를 구현했었습니다

클린 아키텍처를 클린 아키텍처라고 부를 수 없는 ,,,

 

그리고 Hilt와 클린 아키텍처에 대해 조금 더 공부를 하고 Hilt를 도입해서 다온길 ver2로 제대로 Hilt를 사용한 클린 아키텍처로 마이그레이션을 진행하였습니다 이 과정에서 직접 수동으로 의존성 주입을 해주었을 때와 Hilt를 통해 의존성 주입을 해준 과정을 한번 정리해보려고 합니다 ! 

 

전체 코드를 가져오기엔 너무 많아서 마이그레이션을 하면서 제가 편하다고 느꼈던 부분들을 위주로 가져와보았습니다

 

 

 

👩🏻‍💻 기존 코드

RetrofitInstance (네트워크 연결)

object RetrofitInstance {
    private val authClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .addInterceptor(AuthInterceptor())
            .addInterceptor(HttpLoggingInterceptor{
                Log.d("MyOkHttpLog", it)
            }.setLevel(HttpLoggingInterceptor.Level.BODY))
            .build()
    }
    
    private fun retrofitProvider(): Retrofit = Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create(retrofitMoshi))
        .client(authClient)
        .build()

    fun <T> serviceProvider(apiService: Class<T>): T {
        return retrofitProvider().create(apiService)
    }
}

 

기존에는 네트워크 연결과 관련된 코드들을 object로 RetrofitInstance 라는 싱글톤 객체를 만들어 관리했습니다

 

네트워크 요청에 사용할 인증 및 로그를 관리하는 authClient를 만들고, retrofitProvider() 함수를 통해 직접 Retrofit 인스턴스를 생성해주었습니다. 그리고 serviceProvider() 함수에서 `retrofitProvider()' 로 만든 Retrofit 객체를 통해 API 인터페이스를 생성해 주었습니다.

 

 

PlaceRepository

interface PlaceRepository {
    suspend fun getPlaceDetailInfo(placeId: Long): PlaceDetailInfo
    suspend fun getSearchPlaceResultForList(request: ListSearchOption): List<SearchPlace>
    fun getSearchPlaceResultForMap(request: MapSearchOption): Flow<List<SearchPlace>>
    suspend fun getPlaceDetailInfoGuest(placeId: Long): PlaceDetailInfoGuest
    suspend fun getPlaceMainInfo(areaCode: String, sigunguCode: String): PlaceMainInfo
    suspend fun getPlaceReviewList(placeId: Long, size: Int, page: Int): PlaceReviewInfo

    companion object{
        fun create(): PlaceRepositoryImpl{
            return PlaceRepositoryImpl(
                PlaceDataSource(
                    // serviceProvider 함수를 통해 DataSource에 PlaceService 전달
                    RetrofitInstance.serviceProvider(PlaceService::class.java)
                )
            )
        }
    }
}

 

PlaceRepositoryImpl

class PlaceRepositoryImpl(
    private val placeDataSource: PlaceDataSource
): PlaceRepository {

    override suspend fun getPlaceDetailInfo(placeId: Long): PlaceDetailInfo {
        return placeDataSource.getPlaceDetailInfo(placeId).toDomainModel()
    }

    ...
}

 

이제 PlaceRepository에서 구현체인 PlaceRepositoryImpl 객체를 생성하기 위한 create() 함수를 만들어 주었습니다

 

이때, 흐름을 살펴보자면

1. PlaceRepositoryImpl이 동작하려면 PlaceDataSource가 필요합니다

2. PlaceDataSource는 PlaceService가 필요합니다

-> create 함수는 이 의존 관계를 해결하기 위해, 필요한 객체를 순서대로 생성하고 연결해줍니다 (이 과정에서 PlaceService를 제공하기 위해 RetrofitInstance에서 만들어둔 serviceProvider() 함수를 사용합니다

 

 

HomeViewModelFactory

class HomeViewModelFactory(context: Context) : ViewModelProvider.Factory {
    // create 함수를 사용해 Repository 객체 생성
    private val areaCodeRepository = AreaCodeRepository.create(context)
    private val placeRepository = PlaceRepository.create()
    private val villageCodeRepository = VillageCodeRepository.create(context)

    private val getAreaCodeByNameUseCase = GetAreaCodeByNameUseCase(areaCodeRepository)
    private val getPlaceMainInfoUseCase = GetPlaceMainInfoUseCase(placeRepository)
    private val getSigunguCodeByNameUseCase = GetSigunguCodeByNameUseCase(
        villageCodeRepository
    )

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(HomeViewModel::class.java)) {
            return HomeViewModel(
                getAreaCodeByNameUseCase,
                getPlaceMainInfoUseCase,
                getSigunguCodeByNameUseCase
            ) as T
        }
        throw IllegalArgumentException("unknown ViewModel class")
    }
}

 

위에서 만들어둔 Repository 객체를 생성하는 코드는 ViewModel을 생성하기 위한 ViewModelFactory에서 사용됩니다

 

ViewModelFactory는 ViewModel이 생성될 때 필요한 의존성(Repository, UseCase 등)을 주입하기 위해 사용됩니다. 기본 ViewModelProvider는 매개변수가 없는 생성자만 호출할 수 있기 때문에, 필요한 의존성을 안전하고 일관되게 전달하려면 ViewModelFactory를 통해 생성 과정을 커스터마이징해야 합니다.

 

그렇기 때문에 Repository 및 UseCase 객체를 HomeViewModelFactory 내부에서 수동으로 생성하고 ViewModel에 주입해줍니다

그리고 ViewModel에 수동으로 의존성 주입을 해주기 위한 create 함수를 만들었습니다

 

 

HomeMainFragment

private val viewModel: HomeViewModel by viewModels { HomeViewModelFactory(requireContext()) }

 

마지막으로 Fragment에서 ViewModel을 사용하기 위해 ViewModelFactory를 통해 ViewModel을 직접 생성해줍니다 !

그럼 이제 RetrofitInstance부터 전달된 의존성들을 통해 드디어 Fragment에서 사용자를 위한 데이터를 띄울 수 있게 됩니다

 

 

그렇지만 create 함수를 만들고 의존성을 주입하고, 또 함수를 만들고 의존성을 주입하고 ...

계속 반복되면 유지보수 등 관리가 힘들겠죠..!?

 

 

 

그래서 위 내용들을 Hilt를 활용해 리팩토링한 과정을 살펴보겠습니다

 

 

 

💡 Hilt를 적용한 코드

NetworkModule

@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(@Auth client: OkHttpClient, moshi: Moshi): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .client(client)
            .build()
    }
    
    @Provides
    @Singleton
    fun providePlaceService(retrofit: Retrofit): PlaceService {
        return retrofit.create(PlaceService::class.java)
    }
}

 

기존에 RetrofitInstance에서 serviceProvider 함수를 만들어서 API 객체를 수동으로 생성하고 주입했었데요, 그 과정을 @Provides 어노테이션을 사용해 Retrofit과 API 객체를 제공하며, Hilt가 자동으로 의존성을 주입하도록 바꿔주었습니다

또한 @Singleton 어노테이션을 사용하여 싱글톤으로 관리해주었습니다

 

위 과정을 통해 RetrofitInstance.serviceProvider()를 직접 호출할 필요가 없어졌습니다

 

 

PlaceDataSource

internal class PlaceDataSource @Inject constructor(
    private val placeService: PlaceService
){
    suspend fun getPlaceMainInfo(areaCode: String, sigunguCode: String): Result<PlaceMainInfo> = execute {
        placeService.getPlaceMainInfo(areaCode, sigunguCode).toDomainModel()
    }
    ...
}

 

PlaceRepositoryImpl

internal class PlaceRepositoryImpl @Inject constructor(
    private val placeDataSource: PlaceDataSource
) : PlaceRepository {
    override suspend fun getPlaceMainInfo(
        areaCode: String,
        sigunguCode: String
    ): Result<PlaceMainInfo> {
        return placeDataSource.getPlaceMainInfo(areaCode, sigunguCode)
    }
    ...
}

 

PlaceRepository

interface PlaceRepository {
    suspend fun getPlaceMainInfo(areaCode: String, sigunguCode: String): Result<PlaceMainInfo>
}

 

PlaceDataSource 클래스의 생성자에 @Inject 어노테이션을 사용하여 NetworkModule에서 제공해준 PlaceService 객체를 자동으로 주입받습니다

 

그리고 PlaceRepositoryImpl 또한 PlaceDataSource를 @Inject 어노테이션을 통해 생성자로 주입받습니다

 

이 과정을 통해서 기존 코드에서 PlaceRepository 내에서 직접 create() 함수를 만들어 PlaceDataSource(RetrofitInstance.serviceProvider(PlaceService::class.java)) 와 같은 방식으로 PlaceService를 PlaceDataSource에 전달하고, 그 후 PlaceDataSource를 PlaceRepositoryImpl에 전달하는 코드를 생략할 수 있게 되었습니다

 

이제 PlaceRepository를 사용할 때 Hilt에서 알아서 PlaceRepository를 구현한 PlaceRepositoryImpl을 찾아 의존성을 주입해주게 됩니다

 

🔎 과정을 한번 정리해보면

 

위 상황에서는 Hilt가 다음과 같은 연결을 설정합니다

 

  1. PlaceRepository 인터페이스 → PlaceRepositoryImpl 구현체
  2. PlaceRepositoryImpl → PlaceDataSource
  3. PlaceDataSource → PlaceService

Hilt는 이러한 연결을 바탕으로

 

  • PlaceRepositoryImpl 객체를 생성할 때 PlaceDataSource를 전달
  • PlaceDataSource를 생성할 때 PlaceService를 전달
  • 이를 최종적으로 PlaceRepository 타입으로 제공

 

 

HomeViewModel

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val appThemeRepository: AppThemeRepository,
    private val placeRepository: PlaceRepository,
    private val areaCodeRepository: AreaCodeRepository,
    private val sigunguCodeRepository: SigunguCodeRepository,
    private val activationRepository: ActivationRepository,
    private val naverMapRepository: NaverMapRepository
) : ViewModel() {
    
        fun getPlaceMain(area: String, sigungu: String) = viewModelScope.launch(Dispatchers.IO) {
        
            var areaCode = getAreaCode(area)
            var sigunguCode = areaCode?.let { getSigunguCode(sigungu, it) }
        
            if (areaCode != null && sigunguCode != null) {
                placeRepository.getPlaceMainInfo(areaCode, sigunguCode).onSuccess {
                    _aroundPlaceInfo.postValue(it.aroundPlaceList)
                    _recommendPlaceInfo.postValue(it.recommendPlaceList)

                    networkErrorDelegate.handleNetworkSuccess()
                }.onError {
                    networkErrorDelegate.handleNetworkError(it)
                }
            }
        }
    ...
}

 

그리고 이제 ViewModel을 만들 때 더이상 ViewModelFactory를 이용해서 만들어주지 않아도 됩니다 ! 매번 뷰모델을 만들때마다 직접 뷰모델 팩토리를 관리하는 것이 저는 제일 귀찮았던 부분이었는데요, 그래서 그런지 뷰모델을 만들 때 가장 Hilt의 편리함을 느꼈던 것 같습니다

 

@HiltViewModel 어노테이션을 사용하면 Hilt는 컴파일 시점에 ViewModelFactory를 자동으로 생성합니다

우선, HomeViwModel이 Hilt 관리 대상임을 확인하고, 이 뷰모델에 필요한 의존성들을 분석하고 전달합니다 (PlaceRepository 등..)

그리고 ViewModelProvider.Factory를 자동으로 생성해 의존성을 주입해주게 됩니다

 

 

public static HomeViewModel_Factory create(
    Provider<AppThemeRepository> appThemeRepositoryProvider,
    Provider<PlaceRepository> placeRepositoryProvider,
    Provider<AreaCodeRepository> areaCodeRepositoryProvider,
    Provider<SigunguCodeRepository> sigunguCodeRepositoryProvider,
    Provider<ActivationRepository> activationRepositoryProvider,
    Provider<NaverMapRepository> naverMapRepositoryProvider,
    Provider<NetworkErrorDelegate> networkErrorDelegateProvider) {
  return new HomeViewModel_Factory(appThemeRepositoryProvider, placeRepositoryProvider, areaCodeRepositoryProvider, sigunguCodeRepositoryProvider, activationRepositoryProvider, naverMapRepositoryProvider, networkErrorDelegateProvider);
}

 

Hilt가 자동으로 생성해준 ViewModelFactory 코드를 내부를 조금 들여다보니 기존 코드에서 직접 만들어줬던 create 함수처럼 Factory가 직접 생성을 해주고 있는 것 같네요 !

 

 

HomeMainFragment

private val viewModel: HomeViewModel by viewModels()

 

뿐만 아니라 Activity나 Fragment에서 ViewModel을 생성할 때에도 훨씬 간편해졌는데요,

ViewModelFactory를 직접 구현해서 ViewModel을 만들 때 전달해줘야 할 필요가 없어졌으므로 코드가 훨씬 간단해졌습니다

 

내부 코드가 추가되어도 직접 Factory를 관리해주지 않아도 되니 더욱 편리해졌습니다 ㅎㅎ

 

 

 

🍀 마무리

처음에 수동 의존성 주입을 해줄 땐 DI를 써본적이 없어서 불편함을 몰랐는데 이 글을 정리하면서 직접 비교해보니 다시금 수동 의존성 주입의 불편함을 깨달았네요 ㅎㅎ

 

Hilt를 사용하지 않고 다온길 ver1을 구현할 때에는 남들이 다 사용하는 기술을 빠르게 사용하지 못하는 것 같아서 조급하기도 하고 걱정도 되었는데 오히려 Hilt를 사용하지 않고 구현하고, 다시 리팩토링 하면서 Hilt를 적용한 과정이 더 도움이 된 것 같습니다

 

이 과정이 없었으면 Hilt의 편리함을 느끼지 못하지 않았을까 싶네요 ㅎㅎ

 

다온길 ver 1 코드는 여기서 !

https://github.com/Journey-Together/Android

 

GitHub - Journey-Together/Android: 안드드드드드르르르륵탁 🪓

안드드드드드르르르륵탁 🪓. Contribute to Journey-Together/Android development by creating an account on GitHub.

github.com

 

다온길 ver2 코드는 여기서! (Hilt 사용)

https://github.com/Journey-Together/DaOnGil_CleanArchitecture

 

GitHub - Journey-Together/DaOnGil_CleanArchitecture: 다온길 프로젝트 클린 아키텍처(Hilt) ver 🧼

다온길 프로젝트 클린 아키텍처(Hilt) ver 🧼. Contribute to Journey-Together/DaOnGil_CleanArchitecture development by creating an account on GitHub.

github.com