개발일기

Kotlin Retrofit + Coroutines 네트워크 요청 안전 처리

뱅우 2025. 8. 29. 09:43
반응형
Kotlin Retrofit + Coroutines 네트워크 요청 안전 처리

안드로이드 앱에서 네트워크 요청은 흔하면서도 실수하기 쉬운 영역입니다. RetrofitKotlin Coroutines를 조합하면 간결하고 안전한 네트워크 계층을 만들 수 있습니다. 이번 글에서는 핵심 개념과 실무 예제, 에러 처리 및 테스트 팁까지 정리해 보겠습니다.

1. 기본 구성 (Retrofit + Coroutines)

Retrofit을 Coroutine과 함께 쓰려면 suspend 함수를 선언하는 방식이 가장 깔끔합니다. 필수 의존성은 Retrofit, OkHttp, kotlinx-coroutines, 그리고 Gson/Moshi 같은 JSON 파서가 있습니다.

Gradle 의존성 예

implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0")

API 인터페이스

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): UserDto

    @POST("posts")
    suspend fun createPost(@Body request: PostRequest): PostResponse
}

Retrofit 인스턴스 생성

fun provideRetrofit(): Retrofit {
    val client = OkHttpClient.Builder()
        .callTimeout(30, TimeUnit.SECONDS)
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BASIC
        })
        .build()

    return Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
}

2. 안전한 호출: Result 래핑과 예외 처리

네트워크 호출은 실패할 가능성이 항상 있으므로, 결과를 Result 또는 커스텀 sealed class로 감싸 처리하는 것이 좋습니다.

NetworkResult 유틸 예시

sealed class NetworkResult<out T> {
    data class Success<T>(val data: T): NetworkResult<T>()
    data class Error(val exception: Throwable, val code: Int? = null): NetworkResult<Nothing>()
}

suspend fun <T> safeApiCall(call: suspend () -> T): NetworkResult<T> {
    return try {
        val resp = call()
        NetworkResult.Success(resp)
    } catch (e: HttpException) {
        NetworkResult.Error(e, e.code())
    } catch (e: IOException) {
        NetworkResult.Error(e)
    } catch (e: Exception) {
        NetworkResult.Error(e)
    }
}

사용 예

class UserRepository(private val api: ApiService) {
    suspend fun fetchUser(id: String): NetworkResult<UserDto> {
        return safeApiCall { api.getUser(id) }
    }
}

3. 리프레시 토큰/인증 처리 전략

401 응답이 오면 토큰 갱신을 진행해야 합니다. OkHttp Interceptor에서 중앙화하면 코드 관리가 훨씬 수월합니다.

AuthInterceptor 예시

class AuthInterceptor(
    private val tokenProvider: TokenProvider,
    private val refreshApi: AuthApi
): Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${tokenProvider.accessToken}")
            .build()

        var response = chain.proceed(request)
        if (response.code == 401) {
            synchronized(this) {
                val newToken = runBlocking { tokenProvider.refreshTokenIfNeeded(refreshApi) }
                request = request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
                response.close()
                response = chain.proceed(request)
            }
        }
        return response
    }
}

4. 테스트와 모킹

Retrofit 인터페이스는 쉽게 모킹할 수 있습니다. 코루틴 기반 함수는 runBlocking 또는 코루틴 테스트 라이브러리를 활용합니다.

테스트 예시

@Test
fun fetchUser_success_returnsUser() = runBlocking {
    val mockApi = mock<ApiService>()
    val expected = UserDto("1", "Alice")
    whenever(mockApi.getUser("1")).thenReturn(expected)

    val repo = UserRepository(mockApi)
    val result = repo.fetchUser("1")

    assertTrue(result is NetworkResult.Success)
    assertEquals(expected, (result as NetworkResult.Success).data)
}

5. 정리

  • Retrofit + Coroutines 조합은 코드 가독성과 생산성을 크게 높인다.
  • 예외 처리를 Result 형태로 통일하면 UI 계층에서 다루기 편하다.
  • 토큰 갱신은 Interceptor를 활용해 중앙화하자.
  • 테스트는 인터페이스 모킹과 코루틴 테스트 라이브러리를 이용하자.

마무리: 안드로이드 네트워크 계층을 Compose 시대에 맞게 단순화하려면, Retrofit + Coroutines는 사실상 표준입니다.

반응형