안드로이드 Hilt to Koin 전환기

박병준·2024년 8월 1일

KMM

목록 보기
1/2
post-thumbnail

동아리의 프로젝트인 스마트스쿨 플렛폼 도담도담 에서 새로운 버전인 dodam-one을 출시하면서 이슈에 부딪히게 되었다.

대구소프트웨어 마이스터 고등학교 5기부터 이어져온 도담도담은 당시 선생님이 사용하시는 폰의 비율이 안드로이드가 압도적으로 높았기에, iOS버전을 제외한 안드로이드 버전만 제작되었다.
지금까지 도담 티쳐는 iOS의 수요가 확실히 없었다. 하지만, 이번년도 선생님이 많이 들어오시면서, iOS의 비율이 늘었다.

이를 iOS팀에서 대처해도 됬지만, 아직 iOS에서 이슈가 완전히 해결되지 않았기도 하였고, 안드로이드는 이를 미리 고려해 멀티모듈과 Compose를 이용하여 학생용 앱을 제작하였다.

고려하지 않았던 문제가 생겼는데... 바로 Hilt 라는 DI 라이브러리를 사용하였던 것이였다.
Hilt는 안드로이드에 종속성이 있어, 우리가 추구하는 Compose Multi Platform에 맞지 않았다.
그래서 기존 앱에 존재하는 안드로이드의 종속성을 모두 제거하고, KMP에 호환되는 Koin을 도입하기로 하였다.

Hilt를 차츰 Koin으로 전환하기


점진적으로 진행하기 위해선 hilt와 koin 즉 di 두가지를 같이 사용했을 때 빌드, 런타임에서 에러가 발생하지 않느냐가 관건이였다. 결론은 큰 문제는 보이지 않았다.
직접 성능적으로 체크하지는 않았지만 빌드, 런타임 과정에서 문제가 발생하지 않았다.

그래서 실제로 사용하는 코드를 전환하기로 하였다.
먼저 기존 Hilt Module들을 Koin 모듈로 먼저 전환해야했다.

기존 RepositoryImpl은 아래와 같다.

internal class NightStudyRepositoryImpl @Inject constructor(
    private val remote: NightStudyDataSource,
    @Dispatcher(DispatcherType.IO) private val dispatcher: CoroutineDispatcher,
) : NightStudyRepository {}
@Module
@InstallIn(SingletonComponent::class)
internal interface RepositoryModule {
    @Singleton
    @Binds
    fun bindsNightStudyRepository(nightStudyRepositoryImpl: NightStudyRepositoryImpl): NightStudyRepository
}

위와 같이 Repository를 싱글톤으로 DI를 부여했다면, Koin에서는 아래와 같이 작업할 수 있다.

val nightStudyRepositoryModule = module {
    single<NightStudyRepository> {
        NightStudyRepositoryImpl(get(), get(named(DispatcherType.IO)))
    }
}

또 RepositoryImpl에는 매개변수로 DataSource, CoroutineDispatcher를 받아오는데, 이를 위해 get() 을 활용할 수 있다.

호출마다 다른 객체가 생성되야 한다면 factory 키워드를 사용해 DI를 부여할 수 있다.

val nightStudyRepositoryModule = module {
    factory<NightStudyRepository> {
        NightStudyRepositoryImpl(get(), get(named(DispatcherType.IO)))
    }
}

이렇게 한다면 호출마다 새로운 객체가 생성되어 할당된다.

named 가 뭐지?

같은 CoroutineDispatcher라 해도 IO, Main, Default등 주입하고 싶은 것이 여러가지인 경우가 있다. 이떄 named() 라는 함수를 통해 주입될 객체를 지정할 수 있다.

실제 사용

이후 Application의 startKoin 부분에서 해당 모듈을 불러오면 된다.

@HiltAndroidApp
class DodamApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger(Level.DEBUG)
            androidContext(this@DodamApplication)
            modules(
                nightStudyRepositoryModule
            )
        }
    }   
}

실제 ViewModel에서 사용하기 위해서는 먼저 KoinComponent 인터페이스를 ViewModel에서 상속하고, 위에서 get() 키워드를 사용하거나, by Inject() 를 통하여 구현체를 주입받을 수 있다.

@HiltViewModel
class AskNightStudyViewModel @Inject constructor() : ViewModel(), KoinComponent {

    private val nightStudyRepository: NightStudyRepository by inject()
}

Hilt 제거하기.

Hilt였던 모듈들을 제거했다면, 이제는 의존성을 완전히 배제할 차례다.
현재 프로젝트는 Compose로 이루어져 있기에, koin-androidx-compose 모듈을 추가하였다.

먼저 제거할 모듈들은 다음과 같다.
hilt-navigation-compose , hilt-core , hilt-android , hilt-compiler , hilt-plugin
그리고 플러그인에서는
com.google.dagger.hilt.android를 지웠다.

tmi지만, hilt-navigation-compose 이 있는지 모르고 hilt가 왜 안지워지지 하고 1시간동안 사투를 벌였다.

이후 어노테이션에 존재하는 @HiltAndroidApp, @HiltViewModel, @Inject 키워드 들을 모두 제거한다.

viewmodel 설정

기존 compose에서 hilt-navigation-compose 덕분에 hiltViewModel() 한줄로 의존성을 주입했지만, Koin에서는 조금 다르다.

@Composable
internal fun AskNightStudyScreen(
    viewModel: AskNightStudyViewModel = hiltViewModel()
)

이렇게 주입했다면

@Composable
internal fun AskNightStudyScreen(
    viewModel: AskNightStudyViewModel = koinViewModel()
)

이렇게 koinViewModel로 바꿔주면 된다. 그후 다음과 같이 viewModelModule을 생성하고, application에서 주입하면 해결된다.

val askNightStudyViewModelModule = module {
    viewModel { AskNightStudyViewModel() }
}

class DodamApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger(Level.DEBUG)
            androidContext(this@DodamApplication)
            modules(
                nightStudyRepositoryModule,
                askNightStudyViewModelModule
            )
        }
    }   
}

빌드 시간

hilt를 제거하고, Koin으로 전환후 빌드시간은 github action을 기준으로, 약 1분 정도 줄어들었다.

before
after

profile
앱 개발자 박병준입니다.

0개의 댓글