반응형

 

힐트 Hilt - Android 모듈 정리

Hilt는 의존성을 추가하는 (dependency injection, di)는 라이브러리이다. ''의존''이란 한 클래스가 다른 클래스를 참고한다는 뜻인데, 개발을 하다보면 여러 클래스끼리 연결되는 경우가 흔하게 있다. 한 클래스가 준비되어야 다른 클래스도 쓸 수 있는 것이다.

2개 정도의 클래스면 손으로 클래스끼리 연결을 해 줄 수도 있지만, 클래스의 수가 3개, 4개가 넘어가고 여러곳에서 연결이 생기면 일일이 객체를 만드는 것도 번거로운 일이 된다.

이런 어려움을 해결하기 위해 라이브러리를 써서 의존성을 관리한다. 라이브러리를 쓰면 알아서 객체를 만들어준다고, 연결해야될 클래스끼리 연결해준다는 뜻이다. 힐트(Hilt)는 안드로이드에서 쉽게 의존성을 관리할 수 있게 해주는 라이브러리이다. 기존에 많이 쓰이던 대거(Dagger)를 기반으로 만들어졌는데 쓰기가 훨씬 편하다.

사실 대거는 학습 난이도도 꽤 있고, 쓰다보면 일을 줄이려다 더 복잡하게 일을 만들어버리는 경향이 있었는데 힐트는 대거에 비해 상당히 단순하다.

구글에서도 적극 힐트를 밀고 있으니 빨리 힐트를 학습해보도록 하자.

설정하기

힐트 라이브러리를 추가해줘야 힐트를 쓸 수 있다. 2군데에 추가하면 된다

build.gradle - 프로젝트의 그래들

buildscript {
    ext.hilt_version = '2.38.1'
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

app/build.gradle - 앱의 그래들

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'


dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}

설정이 되었으면 힐트를 써보자.힐트를 사용하려면 힐트를 쓴다는 걸 알려줘야한다. 다들 알다시피 안드로이드의 시작점은 Application이다.

Application에 @HiltAndroidApp 이라고 적으면 힐트를 쓸 준비가 된다.

MainApplication.kt 파일

@HiltAndroidApp
class MainApplication: Application() {

}

MainApplication도 잊지말고 AndroidManifest에 추가해주자

AndroidManifest.xml 파일

<application

 android:name=".MainApplication"
 …. 
/>

이제 실행해보자. 잘 돌아가면 본격적으로 힐트를 적용해보겠다


일단 기본적인 액티비티를 만들어보고 설명을 해보겠다.

2개 파일을 만들건데 데이터를 담을 Store 클래스와 MainActivity 클래스이다.

힐트 적용전

Store.kt

const val TAG = "STORE"

class Store {

    fun open() {
        Log.i(TAG, "OPEN")
    }

    fun close() {
        Log.i(TAG, "CLOSE")
    }   
}

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   lateinit var store: Store

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        store = Store() // 스토어 객체를 만듬
        store.opne()
   }
}

액티비티를 보면 못 보던 것이 있다. 바로 @AndroidEntryPoint 이다. @AndroidEntryPoint 는 액티비티나 프래그먼트 등에 붙여서 힐트가 쓰인다는 걸 알려준다.

현재까지는 액티비티(Activity), 프레그먼트(Fragment) , 뷰(View), 서비스(Service), 브로드캐스트(Broadcast)에 붙일 수 있다.

메인 액티비티에서 Store 클래스를 쓰려면 어떻게 해야할까? 보통때라면 미리 객체를 만들거나, onCreate 등 라이프 사이클에서 객체를 만들어 쓸 것이다.

지금은 Store 객체를 하나만 만들지만, Book 객체, Street 객체, Shop 객체 등 온갖 객체를 만들어야 된다고 상상해보자. 매우 번거로울 것이다.

힐트 적용하기

Store.kt

class Store @Inject constructor() {
    fun open() {
        Log.i(TAG, "OPEN")
    }

    fun close() {
        Log.i(TAG, "CLOSE")
    }
}

클래스 이름 뒤에 @Inject 를 적어주고 constructor() 를 적어주면은 된다. 여기서 @Inject 를 적으면 힐트가 이 클래스는 어딘가에서 쓰이겠구나를 알게 된다. 보통 코틀린에서 변수없는 생성자 (constructor)는 생략할 수 있는데 힐트를 쓸때는 꼭 constructor()를 써줘야한다

이제 액티비티에도 힐트를 적용해보자

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

   @Inject lateinit var store: Store

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        store.opne()
   }
}

사용할 객체 앞에 @Inject 를 적어주면 힐트가 알아서 객체를 만들어준다.

여기서는 lateinit var store 앞에 @Inject 를 적어주자

이제 준비가 되었으니 앱을 실행해보자.

별 이상없이 로그가 찍히면 성공이다.

일반 클래스일 경우에는 이처럼 간단히 힐트를 적용할 수 있으나 인터페이스인 경우에는 조금 더 할 작업이 있다

인터페이스에 힐트 적용하기

가게 (Store)를 한종류만 열줄 알았는데, 옷가게 (Clothing Store)와 책방 (Book Store)도 열게 되었다. 옷가게와 책방은 비슷한점도 있기에 기존의 것을 재활용하고 싶다. 그래서 Store 클래스를 수정하고 ClothingStore와 BookStore 클래스를 만든다.

Store.kt

interface Store  {
    fun open()
    fun close()
}

인터페이스로 수정하였다.

ClothingStore.kt

class ClothingStore @Inject constructor() : Store {
    override fun open() {
        Log.i(TAG, "open clothing store")
    }

    override fun close() {
        Log.i(TAG, "close clothing store")
    }

}

Store 인터페이스를 구현하고 @Inject 를 적어준다

BookStore.kt

class BookStore @Inject constructor() : Store {
    override fun open() {
        Log.i(TAG, "open book store")
    }

    override fun close() {
        Log.i(TAG, "close book store")
    }

}

ClothingStore와 동일한 작업을 해준다

이제 액티비티로 가서 객체가 잘 만들어지는지 확인해보자

MainAcitivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var store: BookStore // 잘 만들어진다 

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        store.open() // 에러 없음 
    }
}

구현된 BookStore의 경우 객체가 잘 만들어진다. 힐트가 제대로 일을 한다.

반면 Store 인터페이스를 쓰려고 하면 빌드 단계에서부터 막힌다.

MainAcitivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var store: Store // 에러 발

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        store.open()
    }
}

힐트가 보았을때 Store가 BookStore인지, ClothingStore인지 구분할 수 없기에 에러가 생기는 것이다.

힐트가 이를 구분할 수 있게 하려면 '모듈'(Module)을 추가해주어야한다.

StoreModule.kt

@Module
@InstallIn(ActivityComponent::class) // 모듈이 사용되는 범위를 정한다 
abstract class StoreModule {

    @Binds // 연결한다. 인터페이스의 구현을 제공한다고 보아도 된 
    abstract fun BookStoreImpl(bookStore: BookStore): Store
}

여기서 @Module은 모듈로 사용한다는 의미이고, @InstallIn은 모듈이 어떤 범위에서 쓰이는가를 나타낸다. 범위에는 액티비티, 애플리케이션, 싱글턴 등 다양한 종류가 있으니 입맛에 맞게 골라쓰면 된다.

모듈을 추가했으면 다시 빌드를 해보자. 깔끔하게 빌드가 된다. 실행을 해도 잘된다. 힐트 정복이라고 말하고 끝내고 싶지만 문제가 남아 있다. 아직 Store가 BookStore인 ClothingStore인지 구분을 못하는 것이다. 난감하다. 기껏 인터페이스를 만든 의미가 없다.

다행히 퀄리파이어 (@Qualifier, 구분자) 추가하면 구분을 할 수 있게 된다.

StoreModule을 수정해주자.

StoreModule.kt

@Module
@InstallIn(ActivityComponent::class)
abstract class StoreModule {

    @BookStoreQualifier
    @Binds // @Binds 는 연결한다는 뜻 
    abstract fun BookStoreImpl(bookStore: BookStore): Store

    @ClothingStoreQualifier
    @Binds
    abstract fun ClothingStoreImpl(clothingStore: ClothingStore): Store
}



@Qualifier
annotation class BookStoreQualifier

@Qualifier
annotation class ClothingStoreQualifier

BookStoreQualifier와 ClothingStoreQualifier 라는 퀄리파이어를 만들었다.이 퀄리파이어를 각각의 다른 함수 BookStoreImpl와 ClothingStoreImpl에 붙여주면 이 표시(어노테이션)을 보고 어떤 인터페이스가 어떻게 구현되는지 힐트가 구분할 수 있게 된다.

이제 액티비티로 가보자.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {


    @BookStoreQualifier // 퀄리파이어 사용 
    @Inject lateinit var bookStore: Store

    @ClothingStoreQualifier  // 퀄리파이어 사용 
    @Inject lateinit var clothingStore: Store

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bookStore.open()
        clothingStore.open()
   }
}

위의 액티비티와는 코드가 좀 달라졌다. BookStore를 사용하고 싶을때는 BookStore를 구현한 퀄리파이어 @BookStoreQualifier 를 붙였고, ClothingStore를 사용하고 싶을때는 @ClothingStoreQualifier를 붙였다.

실행을 해보면 BookStore와 ClothingStore가 잘 구분이 되는 걸 알 수 있다. 각기 다른 로그가 나오면 성공이다.

직접 만든 클래스와 인터페이스를 연결하는 법을 알아보았다.

외부 라이브러리를 쓸때는 어떻게 해야할까? Retrofit이나 OkHttp 등등을 사용할때는 어떻게 해야할까

외부 라이브러리 모듈로 만들기

외부 라이브러리에 힐트를 적용하는건 약간 더 까다롭지만 어렵지는 않다.

많이 쓰이는 네트워크 라이브러리인 OkHttp 에 힐트를 적용해보겠다.

app의 build.gradle에 다음과 같이 추가해주자

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'

그리고 네트워크 관련된 코드를 담을 NetworkModule.kt 파일을 만들어보자.

NetworkModule 파일에는 OkHttpClient 객체를 만드는 코드가 들어있다.

일단 코드를 보고 설명을 읽어보도록 하자

NetworkModule.kt

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {

    @Provides // 다른 곳에서 사용될 객체를 만들어준다는 뜻이다  
    @Singleton
    fun provideHttpLoggingInterceptor() : HttpLoggingInterceptor =
        HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)

    @Provides
    @Singleton
    fun provideOkHttpClient(
        @ApplicationContext context: Context,
        httpLoggingInterceptor: HttpLoggingInterceptor
    ): OkHttpClient =
        OkHttpClient.Builder().apply {
            interceptors().add(httpLoggingInterceptor)
        }.build()

}

위의 StoreModule이 abstract class 였던 것과는 다르게 NetworkModule은 일반 클래스이다. 또 객체를 만들어주는 부분을 @Provides 로 표시하였다. @Provides 를 적어줘야 다른 곳에서 객체가 잘 만들어진다.

provideOkHttpClient() 함수가 OkHttpClient 객체를 만들어주는데, 잘보면 인자를 2개 받는다. 하나는 context인데 @ApplicationContext*가 붙어있다. *@ApplicationContext 를 적어주면 힐트가 알아서 컨텍스트를 찾아주니 매우 편리하다 다른 인자 하나는 httpLoggingInterceptor 인데 바로 위의 provideHttpLoggingInterceptor() 에서 HttpLoggingInterceptor 객체를 만들어준다. 같은 모듈 안에 있는 것도 바로 바로 연결이 되는 것이다.

이제 액티비티로 가서 OkHttpClient 객체가 잘 만들어지는지 확인해보자

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var httpClient: OkHttpClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // request 를 하기 위
        val request = Request.Builder()
            .url("https://www.google.com")
            .header("User-Agent", "OkHttp Example")
            .build()    

       httpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.i(TAG, "Network call error - ${call}, err msg - ${e.message}")
            }

            override fun onResponse(call: Call, response: Response) {
                Log.i(TAG, "Network call - ${response.body}")
            }

        })

    }

}

요청을 하고 기다려보자.

로그에 request body 나오면 성공이다.

이번 글에서는 힐트의 사용법에 대해서 간단하게 알아보았다. 힐트를 쓰려는데

헷갈렸다면 이번 글을 통해 도움을 얻었으면 한다.

안드로이드 젯팩 (jetpack - viewmodel, workmanager)과의 연결이나 범위(scope) 등 좀 더 자세한 내용을 알고 싶다면 공식 문서를 참고하도록 하자

참고

안드로이드 힐트 문서 - https://developer.android.com/training/dependency-injection/hilt-android

대거 힐트 문서 - https://dagger.dev/hilt/view-model

반응형

+ Recent posts