본문 바로가기
개발 이야기/안드로이드 개발

의존성 주입은 Hilt로, 네트워크 통신은 Retrofit으로

by 정선한 2024. 6. 25.
728x90
반응형

Hilt를 사용하다 보면 빌드에러를 자주 보곤 합니다. 뭐 당연한 이야기입니다. Hilt의 에러는 빌드 시에 판단되니까요.
이번 포스팅에서는 네트워크 모듈과 관련된 내용입니다.

Retrofit 을 구현하여 API를 연동할 때, 어떤 레포지토리를 구성해야 하는지, 인터페이스와 Multiple 하게 구현해야 할 때는 어떤 것을 주의해야 하는지 등 전반적인 구현을 다루어볼 예정입니다.

일단 라이브러리를 사용하기 위해 dependency를 추가해줍니다.

  • build.gradle.ktx(:project)
id("com.google.dagger.hilt.android") version "2.48" apply false
  • build.gradle.ktx(:app)
  • hilt와 네트워크 통신 라이브러리인 retrofit과 okhttp를 추가합니다.
    plugins {
        ...

        id("kotlin-kapt")
        id("com.google.dagger.hilt.android")
    }

    android {
        buildFeatures {
            buildConfig = true
        }
    }

    dependencies {
        ...
        // hilt
        implementation ("com.google.dagger:hilt-android:2.48.1")
        kapt ("com.google.dagger:hilt-compiler:2.48.1")

        // retrofit
        implementation ("com.squareup.retrofit2:retrofit:2.9.0")
        implementation ("com.squareup.retrofit2:converter-gson:2.9.0")

        // okhttp
        implementation ("com.squareup.okhttp3:okhttp:4.10.0")
        implementation ("com.squareup.okhttp3:logging-interceptor:4.10.0")
        ...
    }

저는 build.gradle이 편한데, 요즘은 옵션 변경을 안 하면 자동으로 프로젝트 생성할 때, build.gradle.ktx로 생성하여 주는군요.

패키지 구조는 잡기 나름이겠지만, 저는 요런 형태가 익숙해서 이렇게 잡았습니다.

많이 보셨을 패키지 구조 같은데 저는 개인적으로 구관이 명관이라고 생각합니다.

저는 테스트 코드를 사용할 때, jsonplaceholder 사이트에서 API를 받아오는데요. 이번 프로젝트 코드에서도 해당 테스트 API를 들고 와보도록 하겠습니다.

먼저 통신을 위해 Retrofit Instance생성을 위한 RetrofitBuilder와 네트워크 오류 컨트롤을 위한 NetworkInterceptor를 작성해 보도록 하겠습니다.

@Module
@InstallIn(SingletonComponent::class)

Hilt는 각 특성의 @Annotation을 이용하여 의존성을 주입합니다.
때에 맞는 Annotation을 사용하면 되기 때문에 엄청 간단하고 쉽게 의존성 주입을 할 수 있습니다.

object RetrofitBuilder {
    /**
     * TEST API
     */
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class TEST1

    /**
     * Service API
     *
     * 추가될 서비스 API
     * /module/service 패키지에 정의
     */
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class SERVICE

    fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

        OkHttpClient.Builder()
            .followRedirects(false)
            .addInterceptor(loggingInterceptor)
            .addInterceptor(NetworkInterceptor())
            .build()
    } else {
        OkHttpClient.Builder()
            .followRedirects(false)
            .addInterceptor(NetworkInterceptor())
            .build()
    }
}

이때 Hilt와 관련하여 주요하게 보아야 하는 주석이 있는데, 바로 @Qualifier@Provides입니다.

Hilt로 Retrofit 모듈을 구현하여 사용할 때, 현재 프로젝트 구조처럼 한 개의 도메인(base URL)을 사용하는 경우도 있지만, 다른 도메인을 여러 개 처리해야 하는 경우가 생기는데요.

이때 @Qualifier를 통해 Annotation 클래스를 작성하여야 합니다.

    /**
     * TEST API
     */
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class TEST1

이 코드와 같이요.
이제 TEST1로 명명된 Annotation 클래스를 @Provide를 통하여 Injection 시키면 이상 없이 여러 개의 Retrofit 모듈을 사용할 수 있습니다.  이 부분은 아래의 코드들에 구현되어 있습니다.

또한 Andorid의 Retrofit은 OkHttp를 사용하여 HTTP 통신을 수행하고 있기 때문에 구조적으로 Retrofit은 OkHttp를 의존하는 구조로 사용되고 있습니다. 그래서 본질적으로는 OkHttpClient를 먼저 생성하고 Retrofit을 생성할 때, client()에 지정하여 사용합니다

OkHttp의 응답을 변형하기 위한 Interceptor 코드 중, NetworkInterceptor 코드에는 네트워크 예외처리를 위한 내용을 담았습니다.

  • Interceptor는 실제적으로 서버통신이 일어나기 전, 후의 요청을 Intercept 해서 로직을 추가하고 다시 원래의 통신으로 돌아가는 역할을 합니다.
  • [참고] square reference : https://square.github.io/okhttp/features/interceptors/

NetworkInterceptorLoggingInterceptor 내용을 담았는데,
LogginInterceptor는 impomemtation에 추가된 implementation ("com.squareup.okhttp3:logging-interceptor:4.10.0")의 내용으로 네트워크 요청 시 로그를 생성시켜 이를 확인할 수 있도록 합니다.
Debug용 빌드 시로 처리해 주었으니 해당 네트워크 통신 확인을 위해서는 디버그용 빌드로 실행하면 확인이 가능합니다.

class NetworkInterceptor: Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        try {
            val builder: Request.Builder = chain.request().newBuilder()
            return chain.proceed(builder.build())

        } catch (e: Exception) {
            val message: String = when(e) {
                is SocketTimeoutException -> "SocketTimeoutException"
                else -> "Exception"
            }

            return Response.Builder()
                .request(chain.request())
                .protocol(Protocol.HTTP_2)
                .code(SERVER_ERROR_CODE)
                .message(message)
                .body("{$e}".toResponseBody(null))
                .build()
        }
    }
}

이 코드와 연결되는 NetworkModule 코드를 작성하였습니다.
이곳에서 생성한 OkHttpclient() 객체를 Retrofit과 연결합니다.

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

    @Provides
    fun provideTestUrl() = API_TEST_URL

    @Singleton
    @Provides
    @RetrofitBuilder.TEST1
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .client(RetrofitBuilder.provideOkHttpClient())
            .baseUrl(provideTestUrl())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    @RetrofitBuilder.TEST1
    fun provideApiService(@RetrofitBuilder.TEST1 retrofit: Retrofit): NetworkService {
        return retrofit.create(NetworkService::class.java)
    }

    @Singleton
    @Provides
    @RetrofitBuilder.TEST1
    fun provideMainRepository(@RetrofitBuilder.TEST1 apiService: NetworkService)
            = NetworkRepository(apiService)

}

이때에는 baseURL을 지정하여 실제 연결하기 위한 도메인을 연결하도록 합니다.
그 하위로 NetworkService, NetworkRepository 코드를 작성하는데 이 부분이 실제적으로 API와 연결하는 역할을 수행합니다.

interface NetworkService {

    /**
     * TEST API
     * /posts/1/comments
     * /albums/1/photos
     * /users/1/albums
     * /users/1/todos
     * /users/1/posts
     */

    @GET("/posts/1/comments")
    suspend fun getComments(): Response<Any>

    @GET("/albums/1/photos")
    suspend fun getPhotos(): Response<Any>

    @GET("/users/1/albums")
    suspend fun getAlbums(): Response<Any>

}
class NetworkRepository @Inject constructor(
    @RetrofitBuilder.TEST1 private val networkService: NetworkService
) {
    suspend fun getComments() = networkService.getComments()
    suspend fun getPhotos() = networkService.getPhotos()
    suspend fun getAlbums() = networkService.getAlbums()
}

여기까지 되면 기본적인 네트워크 처리로직은 구현이 되었을 텐데, 이걸 테스트를 진행해보려고 합니다.
현재는 데이터 모델을 Any로 처리하도록 하였는데, 테스트를 수행하면서 각 타입에 맞는 데이터 클래스를 정의하고 데이터들을 매핑해보려고 합니다.

연결된 API를 통해 데이터를 가지고 올 수 있도록 MainViewModel 내부에 데이터 처리 로직을 추가합니다.

제가 남긴 성공 시, 로그 결과입니다.

2024-06-25 13:42:57.291 13484-13484 TEST                    com.seonhan.app.kotlin_coroutines    E  Test Albums Data : Response{protocol=h2, code=200, message=, url=https://jsonplaceholder.typicode.com/users/1/albums}
2024-06-25 13:42:57.595 13484-13484 TEST                    com.seonhan.app.kotlin_coroutines    E  Test Albums Data : Response{protocol=h2, code=200, message=, url=https://jsonplaceholder.typicode.com/posts/1/comments}
2024-06-25 13:42:58.045 13484-13484 TEST                    com.seonhan.app.kotlin_coroutines    E  Test Albums Data : Response{protocol=h2, code=200, message=, url=https://jsonplaceholder.typicode.com/albums/1/photos}

각 성공 로그 사이에 logcat으로 확인하면 okhttp.OkHttpClient의 내부 로그를 살펴볼 수 있습니다.
이는 LogginInterceptor에서 처리해 준 내용으로 해당 로그에서는 GET방식으로 처리된 내부 통신 로그를 상세하게 살펴볼 수 있고,
결과 body 또한 확인이 가능합니다.

저는 이 데이터를 통해 데이터 클래스를 정의해 볼 겁니다.
New > Kotlin data class File from JSON을 통해서 데이터 클래스를 생성합니다. 저는 TestModel 파일 내에 이 data class들을 모두 담았습니다.

class TestAlbums : ArrayList<TestAlbumsItem>()
data class TestAlbumsItem(
    val id: Int,
    val title: String,
    val userId: Int
)

class TestComments : ArrayList<TestCommentsItem>()
data class TestCommentsItem(
    val body: String,
    val email: String,
    val id: Int,
    val name: String,
    val postId: Int
)

class TestPhotos : ArrayList<TestPhotosItem>()
data class TestPhotosItem(
    val albumId: Int,
    val id: Int,
    val thumbnailUrl: String,
    val title: String,
    val url: String
)

이에 따라 NetworkService에 있는 Response에 대한 타입도 생성된 데이터 클래스에 맞게 재 정의 해주었습니다.

결과적으로 Hilt와 Retrofit( + OkHttp)를 이용한 네트워크 통신을 구현해 보았습니다.
이제 이 데이들을 변환하고 매핑하면서 화면을 그리거나 데이터를 제어할 수 있습니다.

728x90
반응형