[안드로이드 Hilt 2] - Modules (Binds, Provides, Qualifiers)

이영준·2022년 11월 26일
1

안드로이드_Hilt

목록 보기
2/5

Modules이란?

Modules는 여러 외부 서비스나 외부 객체들을 받아와서 사용하는 속히 말해 복잡한 타입의 클래스, 혹은 interface 주입을 위해서 필요한 애너테이션이다.
즉, 주입을 어떻게, 어디다가 해야하는지를 명시하는 곳이 Modules이다.

그 이유는 interface나 외부 라이브러리 클래스들은 contructor injection을 할 수 없기 때문이다. Interface는 어쩌면 당연한게 구현체가 뭔지 모르면 어떤 타입을 주입해야 하는지 알 수 없기 때문이다.

또한 retrofit, okhttp, room과 같은 서드파티 라이브러리들도 내가 만드는 클래스들이 아니기 때문에 constructor injection이 불가능하다.

아래의 예시로 Modules통해서 inject를 해야하는 클래스, 혹은 interface를 만들어보자. 실제로 아래의 클래스는 예시일뿐 정말 네트워킹 통신 기능이 있지는 않다.

1. Interface를 받는 복잡한 타입의 클래스 생성

Interceptor Interface 생성

네트워크 통신 과정에서 통신 결과값을 수정한다거나, http 통신 timeout 등을 설정하기 위해 interceptor를 구현한다.
Interceptor를 여러 형태로 구현하기 위해서는 interface로 만들어주면 좋다.

interface Interceptor {
    fun log(message : String)
}

그리고 이 인터페이스를 받아 실제 구현하는 부분인 impl 파일을 만들어주자. 이번 예시에서는
CallInterceptorImpl, ResponseInterceptorImpl 두개를 만들어줄 것이고, 의존성에 주입되기 위하여 constructor injection을 해준다.

class ResponseInterceptorImpl @Inject constructor() : Interceptor {
    override fun log(message: String) {
        Log.d(TAG, "log: This is a response interceptor: $message")
    }
}
class CallInterceptorImpl @Inject constructor() : Interceptor {
    override fun log(message: String) {
        Log.d(TAG, "log: This is a call interceptor: $message")
    }
}

빌더 패턴으로 NetworkService 생성

이제 이 인터페이스를 인자로 받는 NetworkService 클래스를 만들어보자.

네트워크 서비스 객체를 만들어볼 것이다. 이는 interceptor 와 함께 네트워크 경로 등등을 담는 객체로 실제 통신을 할 때 불러와 사용할 것이다.

코틀린에서 객체를 생성할 수 있도록 하는 패턴 중에 대표적으로 Builder 패턴이 있다. 복잡한 생성자 매개변수를 가진 객체를 속성 값을 객체.속성(속성값) 스타일로 줄 수 있어 가독성을 높일 수 있는 패턴이다.

class NetworkService private constructor(builder : Builder){

    val protocol : String?
    val host : String?
    val path : String?
    val interceptor : Interceptor?

    // builder의 속성들로 networkservice 값 초기화
    init{
        this.protocol = builder.protocol
        this.host = builder.host
        this.path = builder.path
        this.interceptor = builder.interceptor
    }

    // network service 내의 interceptor의 함수를 호출
    fun performNetworkCall(){
        interceptor?.log("Call performed")
        Log.d(TAG, "performNetworkCall: Network call performed : $this")
    }

    // NetworkService의 값들을 넣어주기 위한 Builder 클래스
    class Builder{
        var protocol : String? = null
            private set
        var host: String? = null
            private set
        var path : String? = null
            private set
        var interceptor : Interceptor? = null
            private set

        fun protocol(protocol : String) = apply { this.protocol = protocol }
        fun host (host : String) = apply { this.host = host }
        fun interceptor (interceptor: Interceptor) = apply { this.interceptor = interceptor }
        fun build () = NetworkService(this)
    }
}

2. Interface 생성

Interface를 받는 클래스 외에도 interface 자체도 modules를 통해 주입을 해야 한다. Interface를 하나 만들어보자.

NetworkAdapter

interface NetworkAdapter {
    fun log(message : String)
}

MyAppNetworkAdapter(NetworkAdapterImpl)

interface의 구현체이다.

class MyAppNetworkAdapter @Inject constructor() : NetworkAdapter {
    override fun log(message: String) {
        Log.d(TAG, "log: MyNetworkAdapter : $message")
    }
}

@Modules, @InstallIn

위의 클래스, 인터페이스들은 Modules를 통해 객체화하는 방법을 명시하는데,

@Module, @InstallIn() 애너테이션을 사용한다.

각 애너테이션 의미

@Module은 클래스 내부의 값들을 의존성 부여를 할 것이라는 것을 의미로 제공자 역할을 하는 클래스에 붙는다.

@InstallIn() @Modlue 과 함께 붙어 어디에서 이 모듈 안의 객체들이 생성될 것인지를 의미한다. 둘 이상의 생성지점을 써줄 수도 있다.
예를 들어 @InstallIn(ApplicationComponent::class, ViewComponent::class)

@Binds로 interface 주입

@Binds 는 interface를 의존성 graph 에 추가해주는 애너테이션이다.

주의점
binding을 제공하는 함수를, 그리고 module 클래스를 abstract로 만들어야 한다. 인터페이스에 대한 객체를 module내에서 만들지 않고 hilt를 통해 어떤 구현체를 받아 구현할지 결정하도록 해야 하기 때문이다.

@Module
@InstallIn(ActivityComponent::class) //network 모듈을 component에 추가하는 것
// 물론 hilt는 component를 명시하지 않도록 해주지만 Module로 선언하는 경우에는 어떤 지점에서 이 모듈 내부의 클래스들이 어디에서 객체화되는지 명시 필요
abstract class NetworkModule {

    // networkAdapter을 인터페이스를 사용하게 된다면 , MyAppNetworkAdapter 클래스를 객체화 하도록 한다는 의미
    // MyAppNetworkAdapter는 constructor inject 되어있기 때문에, 객체화를 알아서 시켜줄 것이다
    @Binds
    abstract fun bindNetworkAdapterImpl(networkAdapterImpl : MyAppNetworkAdapter) : NetworkAdapter

}

이제 모든 액티비티 단에서 NetworkAdapter를 부를 수 있을 것이고, 이를 부를 때 MyAppNetworkAdapter가 생성될 것이다.

Activity에서 Binds 해준 인터페이스 부르기

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

//    @Inject
//    lateinit var databaseAdapter : DatabaseAdapter
    @Inject
    lateinit var networkAdapter: NetworkAdapter

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

//        Log.d(TAG, "onCreate: DatabaseAdapter : $databaseAdapter")
//        databaseAdapter.log("Hello Hilt")
        networkAdapter.log("Interface binding")

    }

//    @Inject
//    fun directToDatabase(databaseService: DatabaseService){
//        databaseService.log("Method injection")
//    }
}

networkAdapter를 인터페이스 타입으로만 선언해주고 구현체를 명시하지 않았지만 실행을 해보면

log: MyNetworkAdapter : Interface binding 와 같은 결과값이 나온다. Hilt에서 자동으로 MyNetworkAdapter 클래스를 생성해준 것이다.

@Provides로 Instance 주입

복잡한 타입(constructor injection으로 주입하기에는 매개변수로 interface를 받는 등 적절하지 않은 클래스들) 혹은 외부 클래스(Retrofit, Room 등)은 @Provides로 주입한다.

우리가 직접 어떻게 객체를 생성하는지를 알려주어서 hilt가 이에 맞게 생성하도록 해주는 것이다.

@Module
@InstallIn(ActivityComponent::class)
class NetworkProvideModule {

    @Provides
    fun provideNetworkService() : NetworkService{
        return NetworkService.Builder()
            .host("google.com")
            .protocol("HTTPS")
            .build()
    }
}

Activity에서 Provides 해준 클래스 부르기

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

//    @Inject
//    lateinit var databaseAdapter : DatabaseAdapter
    @Inject
    lateinit var networkAdapter: NetworkAdapter
    @Inject lateinit var networkService: NetworkService

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

//        Log.d(TAG, "onCreate: DatabaseAdapter : $databaseAdapter")
//        databaseAdapter.log("Hello Hilt")
        networkAdapter.log("Interface binding")
        networkService.performNetworkCall()
        Log.d(TAG, "onCreate: ${networkService.host}")

    }

//    @Inject
//    fun directToDatabase(databaseService: DatabaseService){
//        databaseService.log("Method injection")
//    }
}

결과
performNetworkCall: Network call performed : com.hilt.hiltpractice.network.NetworkService@5066094
onCreate: google.com

위와 같이 NetworkService가 생성되어 보여진다.

Qualifiers

인터페이스의 구현체가 보통 하나가 아닌데, 다양한 구현체를 서로 다른 액티비티에서 부르고 싶다. 이를 위해 필요한 것이 Qualifiers이다.

Qualifiers는 Inject를 할 때 어떤 구현체로 인터페이스를 만들 것인지를 정할 수 있도록 해준다.

Qualifier 만들기

Qualifiers.kt

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class CallInterceptor

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ResponseInterceptor

같은 인터페이스를 구현하는 서로 다른 impl에 대한 주입을 구분하기 위해 이렇게 qualifier 애너테이션을 한 annotation class를 만들어줬다.

이제 이 클래스 명들을 애너테이션으로 하여 provides를 써줄 수 있다.

@Module
@InstallIn(ActivityComponent::class)
class NetworkProvideModule {

    @Provides
    fun provideNetworkService() : NetworkService{
        return NetworkService.Builder()
            .host("google.com")
            .protocol("HTTPS")
            .build()
    }

    @CallInterceptor
    @Provides
    fun provideCallNetworkService() : NetworkService{
        return NetworkService.Builder()
            .host("google.com")
            .protocol("HTTPS")
            .interceptor(CallInterceptorImpl())
            .build()
    }

    @ResponseInterceptor
    @Provides
    fun provideResponseNetworkService() : NetworkService{
        return NetworkService.Builder()
            .host("google.com")
            .protocol("HTTPS")
            .interceptor(ResponseInterceptorImpl())
            .build()
    }
}

위와 같이 모듈에 provides를 annotation class의 명과 함께 달아주면 어떤 NetworkService를 반환하는지를 다르게 하여 만들 수 있다.

Activity에서 사용

@CallInterceptor
@Inject lateinit var networkService: NetworkService

아까처럼 단순히 @Inject만을 넣어주는 것이 아닌 CallInterceptor로 만들어진 networkservice 구현체를 부르도록 액티비티 단에서 작성할 수 있다.

profile
컴퓨터와 교육 그사이 어딘가

2개의 댓글

comment-user-thumbnail
2023년 7월 11일

선생님 정리 완전 잘하세요! 컴퓨터와 교육 그 사이 어딘가 인정해드립니다^^

1개의 답글