다온길 프로젝트를 진행하며 처음엔 클린 아키텍처의 구조를 이해하는 단계라 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가 다음과 같은 연결을 설정합니다
- PlaceRepository 인터페이스 → PlaceRepositoryImpl 구현체
- PlaceRepositoryImpl → PlaceDataSource
- 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
'Android' 카테고리의 다른 글
[Android] UseCase 꼭 필요할까? (0) | 2024.10.23 |
---|---|
[Android] 객체지향 프로그래밍이란? (객체지향 프로그래밍 != SOLID 원칙) (0) | 2024.10.11 |
[Android] Dagger와 Hilt로 자동 의존성 주입 이해하기 (0) | 2024.10.03 |
[Android] 의존성 주입이란? (feat. 수동 의존성 주입) (0) | 2024.10.01 |
구글 플레이스토어 첫 앱 출시기 <안뜰> (1) | 2024.09.20 |