backend

Kotlin Retrofit + OkHttp API 통신 완벽 가이드 — 인터셉터와 에러 처리

Android에서 Retrofit과 OkHttp를 조합해 API 통신을 설정하고 인터셉터로 인증 헤더를 추가하며 Coroutines와 연동해 에러를 처리하는 방법을 정리하였다.

★★★★★적극 추천
AndroidRetrofitOkHttpKotlinCoroutines
Kotlin Retrofit + OkHttp API 통신 완벽 가이드 — 인터셉터와 에러 처리
  • ·Retrofit 2는 인터페이스 메서드의 반환 타입으로 suspend fun과 Call<T>를 모두 지원한다
  • ·OkHttp Interceptor는 요청 전후에 헤더 추가, 로깅, 재시도 로직을 삽입할 수 있다
  • ·HttpLoggingInterceptor는 디버그 빌드에서만 BODY 레벨로 설정해야 민감 정보 노출을 방지한다
  • ·Retrofit의 suspend 함수는 HTTP 에러 코드를 자동으로 예외로 변환하지 않아 Response<T>로 래핑하거나 직접 처리해야 한다
프로덕션 앱에서 인증 토큰 만료를 처리할 때 처음에는 각 API 호출마다 401 처리 코드를 복붙했다. 이후 OkHttp Authenticator를 사용해 토큰 갱신을 중앙화하자 중복 코드가 사라지고, 동시에 여러 요청이 만료 토큰을 갱신하려는 경쟁 조건도 synchronized 블록으로 해결할 수 있었다.

Retrofit과 OkHttp 기본 설정

Kotlin Retrofit 싱글턴 설정과 OkHttpClient 빌더 패턴

Retrofit 인스턴스는 앱 전체에서 하나만 생성하는 것이 원칙이다. 여러 곳에서 매번 새 인스턴스를 생성하면 내부 커넥션 풀이 공유되지 않아 소켓 수가 불필요하게 늘어난다. OkHttpClient.Builder()로 클라이언트를 구성하고 Retrofit.Builder().client(okHttpClient)로 연결한다. 타임아웃 설정은 connectTimeout, readTimeout, writeTimeout을 각각 지정하며, 모바일 환경에서는 30초 전후가 일반적이다. addConverterFactory(GsonConverterFactory.create()) 또는 Moshi, Kotlin Serialization 컨버터를 등록해 JSON 파싱을 자동화한다. baseUrl은 반드시 슬래시로 끝나야 상대 URL이 올바르게 합쳐진다는 점을 처음에 모르고 404를 마주하는 경우가 많다.

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)
    .addInterceptor(loggingInterceptor)
    .authenticator(tokenAuthenticator)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl(BuildConfig.BASE_URL)
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

OkHttp Interceptor로 Retrofit 요청에 인증 헤더 자동 추가하기

모든 API 요청에 Authorization 헤더를 추가하려면 OkHttp Application Interceptor를 사용한다. intercept() 메서드에서 chain.request()로 원본 요청을 받고 newBuilder().header("Authorization", "Bearer ${tokenStore.getToken()}").build()로 헤더를 추가한 뒤 chain.proceed()에 전달한다. Network Interceptor는 캐시 히트 시에는 호출되지 않으므로 인증 헤더 추가는 Application Interceptor가 적합하다. 로깅 인터셉터는 HttpLoggingInterceptor(Logger.DEFAULT)를 사용하고 디버그 빌드에서만 Level.BODY로 설정한다. 로그에 액세스 토큰이 노출되지 않도록 프로덕션에서는 Level.NONE 또는 Level.BASIC으로 낮추는 것이 보안 상 필수다. 인터셉터 등록 순서도 중요해서 로깅 인터셉터는 인증 헤더 추가 이후에 등록해야 완성된 헤더가 로그에 찍힌다.

Coroutines와 Retrofit 에러 처리 패턴

Kotlin Coroutines Retrofit suspend 함수 에러 처리 — sealed class 패턴

Retrofit의 suspend 함수는 네트워크 오류나 IOException 발생 시 예외를 던지고, 4xx·5xx HTTP 오류는 기본적으로 예외로 변환되지 않아 Response<T>를 반환 타입으로 쓰거나 직접 isSuccessful 검사가 필요하다. Repository 레이어에서 이를 처리하는 표준 패턴은 sealed class Result<T>를 정의하고 try-catch로 감싸 성공은 Result.Success, 예외는 Result.Error로 변환하는 것이다. ViewModel에서는 이 Result를 when 분기로 처리해 UI 상태를 업데이트한다. HttpException의 code()로 HTTP 상태 코드를 얻을 수 있어 401, 403, 404 등 각 코드에 맞는 사용자 메시지를 표시할 수 있다. 이 패턴을 공통 래퍼 함수로 추출해 모든 API 호출에 재사용하면 에러 처리 코드의 중복을 크게 줄일 수 있다.

suspend fun <T> safeApiCall(call: suspend () -> Response<T>): Result<T> =
    try {
        val response = call()
        if (response.isSuccessful) Result.Success(response.body()!!)
        else Result.Error(HttpException(response))
    } catch (e: IOException) {
        Result.Error(e)
    }

OkHttp Authenticator로 토큰 갱신 자동화

OkHttp Authenticator로 Retrofit 401 응답 시 토큰 자동 갱신 구현하기

Authenticator 인터페이스를 구현하면 401 응답이 오는 즉시 authenticate() 메서드가 호출된다. 여기서 리프레시 토큰으로 새 액세스 토큰을 발급받고 원본 요청에 새 토큰을 붙여 반환하면 OkHttp가 자동으로 재시도한다. 단, 동시에 여러 요청이 401을 받아 모두 갱신을 시도하는 경쟁 조건이 발생할 수 있다. synchronized(this) 블록과 이미 갱신된 토큰과 요청의 토큰을 비교하는 로직을 추가하면, 첫 번째 갱신이 완료된 이후에는 이미 발급된 새 토큰을 그대로 사용해 중복 갱신 요청을 방지할 수 있다. 리프레시 토큰도 만료된 경우에는 null을 반환해 재시도를 중단하고 로그인 화면으로 내보내는 처리를 이벤트로 연결한다.

자주 묻는 질문

Retrofit과 Ktor 중 Android에서 어떤 것을 선택해야 하나요?+

Retrofit은 인터페이스 기반의 선언적 API 정의가 장점이고, Ktor는 멀티플랫폼(KMP)을 지원해 iOS 코드 공유가 목표라면 유리합니다. 순수 Android 앱이라면 Retrofit이 생태계와 레퍼런스가 훨씬 풍부합니다.

Retrofit으로 파일 업로드는 어떻게 구현하나요?+

MultipartBody.Part.createFormData()로 파트를 만들고 @Multipart @POST 어노테이션과 @Part 파라미터를 사용합니다. 대용량 파일은 RequestBody에 스트리밍 처리를 추가해 메모리 초과를 방지해야 합니다.

Retrofit 응답을 캐시하려면 어떻게 하나요?+

OkHttpClient에 Cache 객체를 설정하고 서버가 Cache-Control 헤더를 올바르게 반환하면 자동으로 캐시됩니다. 오프라인 지원이 필요하면 ForceCacheInterceptor를 추가해 네트워크 불가 시 캐시를 강제 사용할 수 있습니다.

관련 글