안드로이드 테스트 어플리케이션 제작중 Hilt를 적용하는 과정에서 오류가 발생했다.
왜 공식 문서와 안드로이드 개발문서의 버전이 다른것인가!
따라서 이번에는 Kotlin DSL 환경에서 Hilt 사용을 위한 적용 과정을 정리해두려 한다.
참고로 아래 적용 방식은 Kotlin DSL을 기본으로 서술됩니다.
다음은 안드로이드 공식 개발문서 와 Hilt 라이브러리 공식 사이트이다
두 문서를 같이 참조하는 이유는 연동 과정은 Hilt 라이브러리 공식 사이트가,
개발 적용과정은 안드로이드 개발 문서에 잘 나와있다. 따라서 두 가이드를 레퍼런스 삼아 연동을 진행한다.
KSP가 뭐야? 라는 질문은 다음 포스트 확인!
https://velog.io/@jun34723/Android-Kapt-KSP
KSP 지원 라이브러리의 Hilt 가 있으니
참조 : https://kotlinlang.org/docs/ksp-overview.html#supported-libraries
두 라이브러리를 프로젝트 수준의 build.Gradle에 추가해준다.
id("com.google.devtools.ksp") version "1.8.0-1.0.9" apply false
id("com.google.dagger.hilt.android") version "2.48" apply false
그럼 다음과 같은 모습으로 적용될 것이다.
Hilt Gradle 플러그인을 사용하면, Hilt에서 생성된 클래스를 직접 참조할 필요가 없어 사용이 간편하다.
또한, 플러그인을 사용하지 않을 경우, 기본 클래스를 주석에 명시하고 생성된 클래스를 확장해야 한다.
예를들어 Hilt 사용을 위한 @HiltAndroidApp 주석을 추가할 때, 플러그인의 차이는 다음과 같다.
플러그인 미적용 :
@HiltAndroidApp(MultiDexApplication::class)
class MyApplication : Hilt_MyApplication()
플러그인 적용 :
@HiltAndroidApp
class MyApplication : MultiDexApplication()
특히 안드로이드 공식 문서에서 위와 같은 플러그인 적용 예제를 안내하고 있다.
(이부분의 빌드 오류로 한참 찾았다;;)
그러니 웬만하면 추가하고 가는것이 좋을것 같다.
이제 모듈수준 build.gradle의 다음과 같이 두 플러그인을 추가해준다.
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
이후 의존성에 다음과 같이 hilt 모듈을 추가해준다
implementation ("com.google.dagger:hilt-android:2.48")
ksp ("com.google.dagger:hilt-android-compiler:2.48")
그럼 Hilt 적용을 위한 과정은 모두 완료되었다.! 다음은 Hilt 사용을 위한 간단한 예제를 안내하겠다.
다음은 Hilt를 이용해 Retrifit를 사용하는 간단한 예제이다.
@HiltAndroidApp은 Hilt를 사용하기 위해 필요한 애플리케이션 클래스를 정의하는 어노테이션이다.
이 어노테이션은 다음과 같은 역할을 한다 :
1. Hilt 컴포넌트 생성: 애플리케이션의 전체 수명 주기를 관리하는 Hilt 컴포넌트를 생성한다.
이 컴포넌트는 애플리케이션의 최상위 컴포넌트로서, 애플리케이션 전역에서 사용할 수 있는 의존성을 제공합니다.
2. DI 설정 초기화: Hilt가 애플리케이션에서 의존성 주입을 사용할 수 있도록 초기 설정을 수행한다.
3. Hilt와 연동된 Android 클래스: Hilt는 @HiltAndroidApp 애노테이션이 적용된 클래스와 연동된 다양한 Android 클래스(Activity, Fragment 등)에서 의존성 주입을 쉽게 할 수 있게 해준다.
따라서 안드로이드 어플리케이션 클래스를 생성하고 해당 클래스에 위 어노테이선을 지정해주어야 한다.
@HiltAndroidApp
class App : Application() {
override fun onCreate() {
super.onCreate()
//그 외 초기화가 필요한 코드들..
}
}
위와같이 Application() 상속 클래스를 생성한 뒤 Manifast에 해당 클래스 이름을 추가해주는것을 잊지 말자!
다음은 Retrifit을 이용하기 위한 객체를 생성해 볼것이다. 우선 다음 예제를 보자
@Module
@InstallIn(SingletonComponent::class)
object RetrofitClient {
private const val EXAMPLE_URL1 = "https://example.api.com/"
private const val EXAMPLE_URL2 = "http://example.local.com/"
@Provides
@Singleton
@Named("BaseURL1")
fun provideRetrofitForBaseURL1(): Retrofit =
Retrofit.Builder()
.baseUrl(EXAMPLE_URL1)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideAPIService(@Named("BaseURL1") retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
@Provides
@Singleton
@Named("BaseURL2")
fun provideRetrofitForBaseURL2(): Retrofit =
Retrofit.Builder()
.baseUrl(EXAMPLE_URL2)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun provideLocalService(@Named("BaseURL2") retrofit: Retrofit): LocalService =
retrofit.create(LocalService::class.java)
}
일반적으로 사용하는 Retrifit 예제랑 살짝 다른점이 있다. 사용하는 BASEURL이 2개 지정 되어있는데, 해당 프로젝트의 요구사항이라 2가지 URL을 사용하는 객체가 필요했다. 해당 과정을 위에서부터 설명해보겠다.
@Module
@Module 애노테이션은 의존성을 제공하는 클래스를 정의할 때 사용한다.
따라서 의존성 객체를 생성하고 제공하는 역할을 하는 클래스에 위 어노테이션이 필요하다.
해당 어노테이션에 대한 특징으로
• 의존성 제공: 모듈은 Hilt가 특정 의존성을 어떻게 생성해야 하는지 정의한다.
• 프로바이더 메서드: 모듈 클래스 안에 의존성을 생성하고 반환하는 메서드(일반적으로 @Provides 애노테이션을 사용)를 포함한다.
따라서 위 코드는 RetrofitClient 객체를 제공하는 방법을 정의했다고 보면 된다.
@InstallIn
@InstallIn 어노테이션은 모듈이 설치될 Hilt 컴포넌트를 지정합니다. Hilt 컴포넌트는 의존성의 생명 주기를 관리하는 역할을 한다 특징으로는,
특징
• 컴포넌트 지정: @InstallIn은 모듈이 어느 컴포넌트에 설치될지를 지정한다.
• 범위(scope): 컴포넌트를 통해 의존성의 생명 주기 범위를 결정합니다. 예를 들어, SingletonComponent는 애플리케이션 생명 주기 동안 의존성을 공유한다.
주요 컴포넌트 종류
• SingletonComponent: 애플리케이션 생명 주기 동안 유지되는 의존성을 제공.
• ActivityComponent: Activity 생명 주기 동안 유지되는 의존성을 제공.
• FragmentComponent: Fragment 생명 주기 동안 유지되는 의존성을 제공.
• ViewModelComponent: ViewModel 생명 주기 동안 유지되는 의존성을 제공.
따라서 위 예시에서 NetworkModule은 Retrofit 인스턴스를 제공하며, SingletonComponent에 설치되어 애플리케이션 생명 주기 동안 Retrofit 인스턴스를 공유한다.
마지막으로 해당 객체가 생성되는 방식을 설명하겠다.
두 객체가 있지만 @Name 차이일 뿐 구성 방식은 같기 때문에
provideRetrofitForBaseURL1 - provideAPIService 하나만 설명을 추가하겠다.
Retrofit 인스턴스 제공 (BaseURL1)
@Provides
@Singleton
@Named("BaseURL1")
fun provideRetrofitForBaseURL1(): Retrofit =
Retrofit.Builder()
.baseUrl(EXAMPLE_URL1)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
Hilt에게 이 메서드가 의존성을 제공한다고 알려준다.
@Singleton
이 의존성이 애플리케이션 생명 주기 동안 한 번만 생성되고 공유되도록 지정한다.
@Named(“BaseURL1”)
Hilt에게 여러 Retrofit 인스턴스 중 어떤 것을 제공하는지 명확히 하기 위해 이름을 지정한다 여기서는 BaseURL1 이라는 이름을 사용했다.
위 과정을 통해 provideRetrofitForBaseURL1()는 EXAMPLE_URL1을 사용하는 Retrofit 인스턴스를 제공하도록 설정되었다. 이제 해당 URL을 사용하는 서비스를 만들어 보자.
ApiService 제공
@Provides
@Singleton
fun provideRewardAPIService(@Named("BaseURL1") retrofit: Retrofit): ApiService =
retrofit.create(ApiService::class.java)
@Provides
이 메서드가 ApiService를 제공한다고 명시한다.
@Singleton
ApiService가 애플리케이션 생명 주기 동안 한 번만 생성되고 공유되도록 한다.
@Named(“BaseURL1”) retrofit: Retrofit
주입된 BaseURL1 이름을 가진 Retrofit 인스턴스를 사용한다.
이제 BaseURL1 Retrofit 인스턴스를 사용하여 ApiService를 생성하였다.
여기서는 @Named 어노테이션을 통해 중복되는 객체를 지정해 줄 수 있다는것이 핵심이다.
이제 거의 끝났다! 해당 객체를 통해 서비스를 호출하기만 하면 된다.
우선 해당 서비스를 호출하는 class 부터 예제를 보자
class VersionChecker @Inject constructor(private val apiService: APIServiceInterface) {
...
}
위 코드는 API 서버에서 버전을 가저오는 행위를 정의한 class이다.
위 클래스는 APIServiceInterface를 주 생성자로 받아들이고 있도록 설정되었다.
그 옆의 @Inject 어노테이션은 Hilt가 이 생성자를 통해 의존성을 주입해야 한다는 것을 나타낸다. 이 애노테이션 덕분에 Hilt는 VersionChecker의 인스턴스를 생성할 때 APIServiceInterface의 구현체를 자동으로 주입한다.
생성자 주입은 가장 일반적이고 권장되는 의존성 주입 방식이다. 이를 통해 VersionChecker 클래스는 의존성을 명확하게 요구하고, 객체 생성 시 모든 필요한 의존성을 전달받을 수 있다.
VersionChecker 클래스는 다른 클래스에서 주입받아 사용된다. 주로 @AndroidEntryPoint 애노테이션이 적용된 Android 구성 요소(Activity, Fragment 등)에서 사용된다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var versionChecker: VersionChecker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// versionChecker 사용
versionChecker.checkVersion()
}
}
위 사용 예제에서는 다음과 같은 과정이 진행된다.
생성자 주입:
VersionChecker 클래스는 생성자 주입을 통해 APIServiceInterface를 주입받는다.
@Inject 어노테이션
@Inject 애노테이션이 생성자에 사용되어 Hilt가 의존성을 주입할 수 있게 한다.
Hilt 모듈 설정:
Hilt 모듈로 구현한 APIServiceInterface객체를 선언한다.
주입된 클래스 사용
VersionChecker 클래스에 정의된 서비스를 호출한다.
이처럼 의존성 주입을 통해 간단하게 네트워크 통신을 구현하는 예제를 정리했다.
특히 체감했던 장점은 결합도 감소이다. 클래스가 직접 의존성을 생성하지 않고 외부에서 주입받기 때문에 클래스 간의 결합도가 낮아지기 때문에 해당 객체에 대한 관리를 줄일 수 있었다.
앞으로 더 많이 사용해보고 정리할 예정이다.