본문 바로가기

Android/Network

[Android] Retrofit 대신 Ktor을 사용하여 통신을 해보자. -2. DI

728x90

본 게시글은 이전 글에 이어서 작성된 부분입니다.

2022.08.12 - [Android/Network] - [Android] Retrofit 대신 Ktor을 사용하여 통신을 해보자.

 

지난 글에 이어서, 이번에는 Hilt와 Koin을 사용하여 Ktor을 적용해보도록 하였다.


 

우선,

Hilt를 사용하기 위해 Gradle에 추가해주도록 한다.

 

plugins {
    ...
    id 'com.google.dagger.hilt.android' version '2.41' apply false
}

 

Project 범위의 Gradle에 Hilt 플러그인을 추가해주고,

 

plugins {
    ...
    id 'dagger.hilt.android.plugin'
}

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

 

Module 범위의 Gradle에 위와 같이 플러그인과 디펜던시를 추가해주도록 한다.

 

그 후, 해야 할 작업들을 작성해보도록 하자.

 

  1. Application 단에 Hilt 등록 및 매니페스트에 등록
  2. 의존성을 주입 받을 액티비티 등록
  3. ViewModel에 의존성 주입
  4. ViewModel 호출에 필요한 매개변수 의존성 주입
  5. 외부 라이브러리 (Ktor) 의존성 주입

5가지 작업을 수행하면 간단하게 Hilt를 적용시켜 Ktor을 사용할 수 있다.

 

순서대로 작업을 진행해보도록 하자.

각 클래스 내부의 코드이전 글에서 설명했으므로 생략하고 진행하도록 하겠다.

 

@HiltAndroidApp
class DIApplication: Application()

 

Application Class를 만들어 @HiltAndroidApp 어노테이션을 추가해주고,

 

android:name=".DIApplication"

 

Manifest에 등록해주도록 한다.

 

다음으로,

 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() { ... } 

 

의존성을 주입 받을 Activity에 EntryPoint 어노테이션을 설정해주고,

 

class MainViewModel @Inject constructor(private val apiInterface: ApiInterface) : ViewModel() { ... }

 

viewModel에도 의존성을 주입해주도록 한다.

 

 

여기서 인자로 ApiInterface를 받아서 사용한다. apiInterface의 함수를 사용하게 되면 Interface의 구현체인 ApiService의 함수가 호출되게 되게 된다. 여기서 인자를 구현체인 Service로 받아도 상관없지만, Interface로 선언된 함수만 사용하겠다.라는 제한을 두고 안정성 있게 사용하기 위해서 Interface를 인자로 받아서 사용하도록 하였다.

 

다음으로는 ApiInterface는 인터페이스이기 때문에 추가로 확인할 부분은 없고 ApiService의 코드를 확인해 보아야 한다.

ApiService의 기존 코드를 보면,

 

private val client = HttpClient(CIO) {
    ...
}

 

형식으로 선언하여 통신에 사용하고 있다.

 

이 부분은 외부 라이브러리 부분이므로 Hilt를 사용하여 의존성을 주입해주기 위해서는 클래스를 만들어서 주입해주는 형식이 아닌, 별도의 Module을 만들어 주입을 해주어 사용해야 한다.

 

따라서,

 

class ApiService @Inject constructor(private val client: HttpClient) : ApiInterface { ... }

 

ApiService에서는 외부 라이브러리를 사용하는 HttpClient를 인자로 받아서 사용하도록 하며, ApiInterface를 상속받아 Override를 통해 client를 사용한 통신 작업을 수행해주면 된다.

 

4번 항목까지 의존성을 주입해주었으므로, 마지막으로 5번인. 외부 라이브러리인 Ktor에 대한 의존성을 주입해주면 된다.

 

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

    @Singleton
    @Provides
    fun provideApiService(client: HttpClient): ApiInterface {
        return ApiService(client)
    }

    @Singleton
    @Provides
    fun provideHttpClient(): HttpClient {
        return HttpClient(CIO) {
            install(Logging) {
                logger = object : Logger {
                    override fun log(message: String) {
                        Log.d("ktorLogger", "message : $message")
                    }
                }
                level = LogLevel.ALL
            }

            install(ContentNegotiation) {
                json(Json {
                    ignoreUnknownKeys = true
                    isLenient = true
                    encodeDefaults = true
                })
            }

            install(HttpTimeout) {
                connectTimeoutMillis = 6000
                requestTimeoutMillis = 6000
                socketTimeoutMillis = 6000
            }

            defaultRequest {
                contentType(ContentType.Application.Json)
                headers {
                    append("X-Naver-Client-Id", "33chRuAiqlSn5hn8tIme")
                    append("X-Naver-Client-Secret", "fyfwt9PCUN")
                }
            }
        }
    }
}

 

 

HttpClient부터 확인해보면 return 타입이 HttpClient로, 기존에 객체로 만들어서 사용했던 코드를 그대로 가져와서 반환해주도록 하였다.

그리고, httpClient를 사용하는 ApiInterface의 구현체 ApiService에 대한 의존성도 주입해주도록 한다.

 

이 때, ViewModel에서 ApiInterface가 아닌 ApiService를 인자로 사용하는 경우 별도로 모듈에 의존성 주입을 위한 provider를 생성하지 않아도 상관없다.

그 이유는 이전에 작성했던 글에서 알 수 있듯이 매개변수로 들어가는 값들의 의존성을 꼬리물기 하면서 주입을 해주어야 하는데, ApiService를 사용하는 경우 HttpClient를 제외하고 명시적으로 의존성 주입이 되기 때문이다.

 


다음으로는,

Koin을 사용하여 의존성 주입을 해보도록 하자.

 

// koin
def koin_version = "3.2.0"
implementation "io.insert-koin:koin-android:$koin_version"

 

Koin을 적용하기 위해 Module 범위의 gradle에 라이브러리를 추가해주도록 하자.

 

Hilt를 통해 의존성 주입을 했던 코드에 Koin도 적용해볼 예정이기 때문에, Hilt에 관련된 코드들을 모두 주석처리를 하고 Koin을 적용해보았으니 참고하길 바란다.

 

우선,

Application 단에 Koin을 실행시키기 위한 startKoin을 선언해준다.

 

startKoin {
    androidContext(this@DIApplication)

    modules(koinModule)
}

 

 

별다른 코드가 없기 때문에, 하나의 koinModule 파일을 생성해서 해당 부분 안에 의존성에 관련된 내용을 넣어주었다.

 

다음으로,

Activity에서 ViewModel에 대한 의존성을 주입했던 부분을 변경해주도록 한다.

 

    // Koin - import org.koin.androidx.viewmodel.ext.android.viewModel
    private val viewModel: MainViewModel by viewModel()
    // Hilt - import androidx.activity.viewModels
//    private val viewModel: MainViewModel by viewModels()

 

이전 Koin에서 Hilt로 마이그레이션을 할 때 작성했던 것처럼, viewModel의 의존성 주입에 사용되는 부분이 다르기 때문에 위처럼 Hilt에서 사용하던 by viewModels()를 사용하지 못하고, by viewModel()을 사용하도록 변경해주어야 한다.

 

이 부분까지 수정하였으면,

koinModule 만 작성해주면 된다.

 

val koinModule: Module = module {
    singleOf(::MainViewModel)
    singleOf(::ApiService) bind ApiInterface::class

    single {
        HttpClient(CIO) {
            install(Logging) {
                logger = object : Logger {
                    override fun log(message: String) {
                        Log.d("ktorLogger", "message : $message")
                    }
                }
                level = LogLevel.ALL
            }

            install(ContentNegotiation) {
                json(Json {
                    ignoreUnknownKeys = true
                    isLenient = true
                    encodeDefaults = true
                })
            }

            install(HttpTimeout) {
                connectTimeoutMillis = 6000
                requestTimeoutMillis = 6000
                socketTimeoutMillis = 6000
            }

            defaultRequest {
                contentType(ContentType.Application.Json)
                headers {
                    append("X-Naver-Client-Id", "33chRuAiqlSn5hn8tIme")
                    append("X-Naver-Client-Secret", "fyfwt9PCUN")
                }
            }
        }
    }
}

 

해당 모듈에서 의존성을 주입해준 것은 HIlt에서 사용한 모듈에서 선언한 것과 다른 부분은 viewModel 관련된 부분 밖에 없다.

 

외부 라이브러리인 HttpClient에 대한 객체 생성과 ApiService에 대한 의존성 주입은 동일하며,

Hilt에서는 @HiltViewModel 과 @Inject constructor를 viewModel에서 의존성 주입을 명시해주었다면,

Koin의 경우 moduel에서 ViewModel에 대한 의존성 주입을 명시해주어야 하기 때문에 이 부분이 차이가 난다.

 

물론, koin에서 viewModel에 대한 의존성 주입은 아주 간단하게 구현할 수 있으며,

 

singleOf(::MainViewModel)
viewModel { MainViewModel(get()) }

 

 

위의 2가지 선언 방법 중 하나를 선택하여 적용하면 된다.


이처럼 Hilt나 Koin을 사용한 의존성 주입은 아주 간단하게 사용할 수 있었다.

 

필자가 Ktor을 DI와 함께 사용해보려고 할 때, Ktor에 대한 의존성 주입을 어떻게 해야 할지 잘 모르겠다.라는 생각이 먼저 들었다. 그래서 구글링을 하며 작업을 진행하였는데, 다양한 자료들이 있음에도 불구하고 Client 기준으로 된 글보다는 Server 기준으로 된 글이 많았고, Kotlin 기반의 코드이기 때문에 그 차이를 모르고 코드를 확인하면서 더 헷갈렸던 것 같다.

 

막상 적용을 해보니 상당히 쉽게 의존성 주입을 통해 Ktor을 사용할 수 있었는데, 서버와 헷갈리다보니 ktor의 의존성 주입은 기존과 많이 다르구나 라는 착각을 하게 되어 많은 시간을 소요했던 것이 아쉽게만 느껴졌다.

Hilt와 Koin 모두 기존의 사용 방법에서의 다른 부분은 없고, 기본적인 사용만 한다면 그저 Ktor 객체를 생성했던 것을 사용하여 주입해주면 된다는 것만 알고 넘어가면 될 것 같다.

 

다음에는 Ktor을 정식적으로 사용하게 된다면, 조금 더 심화된 버전으로 사용한 후 관련된 글을 작성해볼 예정이다.

 

해당 게시글에 사용한 예제 코드는 Github에 올려두었다.

https://github.com/HeeGyeong/KtorSample

 

GitHub - HeeGyeong/KtorSample

Contribute to HeeGyeong/KtorSample development by creating an account on GitHub.

github.com

 

728x90