안드로이드에서 로그인 기능을 붙이고, OAuth로 보호되는 API를 호출하다 보면 거의 반드시 마주치는 문제가 있습니다.
Retrofit 기반 프로젝트에서는 보통 OkHttp 레벨에서 “토큰 만료 → 갱신 → 재요청” 시나리오를 처리합니다.
간단 용어 정리
- OAuth: 비밀번호를 주지 않고도 제3자에게 내 정보 접근(권한)을 허용하는 표준 방식
주로 Access Token, Refresh Token을 사용
- OIDC(OpenID Connect): OAuth 위에 “로그인 인증” 목적을 얹은 표준
ID Token이 추가될 수 있음, OAuth는 권한, OIDC는 인증에 초점
- JWT(Json Web Token): 토큰 포맷(형식) 중 하나
토큰 안에 클레임(유저/권한/만료 등)을 담아 서명한 구조
"OAuth = JWT”는 아니고, OAuth 토큰이 JWT 의 한 형태일 수도 있습니다.
기본적으로 retrofit 을 사용하는 프로젝트에서 OAuth 토큰 만료를 및 갱신 시나리오를 처리하는 경우, 앱의 모든 네트워크 요청이 OkHttp 를 거치기 때문에 헤더에서 토큰을 가로채고 이를 갱신하는 로직을 한번만 구현하면 됩니다.
해당 방식을 해결하기 위한 방법으로 Okhttp 에서 두가지 방법을 지원합니다.
Authenticator 와 Interceptor 가 있습니다.
Authenticator는 OkHttp가 401 혹은 407을 받았을 때 호출합니다.
Authenticator 는 프록시 서버에 대한 인증인 실패 코드인 407, 웹서버 인증 실패 코드인 401 을 응답 받을 때 실행됩니다.

Autenticator 를 구현하기 위해서는 해당 인터페이스를 상속받아 구현해야합니다.
authenticate 메소드를 오버라이딩해서 내부에 인증 fallback 처리를 해줍니다.
내부 구현방식을 보겠습니다.
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already failed to authenticate.
}
String credential = Credentials.basic(...)
return response.request().newBuilder()
.header("Authorization", credential)
.build();
먼저 웹서버 즉, 이미 지난 권한 인증을 시도했는지 확인하는 로직이 필요합니다.
authenticate 로 받은 response 는 이전 요청에 대한 응답을 가지고 있기 때문에 response.request().header("Authorization") 를 통해 이미 인증 헤더를 가지고 있는지 확인합니다.
인증 재시도 로직은 딱한번만 수행해야하기 때문에 위와 같이 인증 요청을 이전에 수행하였는지 파악하여 무한 재시도를 막기 위한 로직을 처리합니다.
좀 더 명시적으로 제한한다고하면 아래와 같이 시도횟수를 카운팅하여 제한하는 방법이 있습니다.
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
제가 이번에 진행한 프로젝트에서는 토큰 재갱신 API 호출 로직이 이미 repostiory 에 정의해 사용하고 있었기에 해당 로직을 그대로 사용하는 방식으로 적용했습니다.
구현은 크게 3가지로 나눌 수 있습니다.
시도횟수 제한, 무한 반복 방지
if (responseCount(response) >= MAX_RESPONSE_COUNT) return null
private fun responseCount(response: Response): Int {
var count = 1
var current = response.priorResponse
while (current != null) {
count++
current = current.priorResponse
}
return count
}
앞서 살펴본 바와 같이 401 이 뜬 후 재요청을 시도 한후 다시 401 이 떠서 해당 로직을 반복할 수 있기에 횟수 제한을 설정하였습니다.
토큰 갱신 요청
synchronized(this) { // 동기화
val newAccessToken = runBlocking {
tryReissueToken()
} ?: return null
return response.request.newBuilder() // 새 accessToken 으로 요청
.header(AUTHORIZATION_HEADER, "$BEARER_PREFIX $newAccessToken")
.build()
}
private suspend fun tryReissueToken(): String? {
val refreshToken = tokenDataStore.getRefreshToken()
?: return handleReissueFailure()
... // 리프레쉬 토큰으로 토큰 재갱신 로직
)
}
갱신시에는 authenticate 가 동기로 선언이 되어 있기 때문에 runBlocking 을 이용해 해 스레드를 일시 블로킹합니다.
토큰 갱신 실패
private suspend fun handleReissueFailure(): String? {
tokenDataStore.clearInfo()
appRestarter.restartApp(isStartLogin = true)
return null
}
갱신 실패시에는 토큰을 전부 제거하고, 다이얼로그를 띄워 로그인 화면으로 이동하거나, Process Phoenix 등을 사용해 앱을 재시작하게 합니다.
마지막으로 해당 TokenAutenticator 를 OkhttpClient 에 설정해줍니다.
val okHttpClient = OkHttpClient.Builder()
.authenticator(TokenAuthenticator(tokenManager))
// 다른 설정 ...
.build()
Authenticator와 달리 Interceptor 는 요청 혹은 응답이 처리되기 전 이를 가로채서 수정하는 방식을 사용합니다. 기존에 401, 407 코드 응답시 호출되었던 Autenticator 와 다르게 직접적으로 상태 코드를 확인해 좀 더 유연하게 처리할 수 있습니다.
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenProvider.getToken() // 현재 토큰 가져오기
// 요청에 토큰 추가하는 로직 생략...
val response = chain.proceed(requestWithToken)
// 토큰 만료시 (401 응답)
if (response.code = 401) {
// ... 이하 토큰 갱신 및 실패 로직 수행
}
}
핵심은 응답 코드를 확인해 앞서 Autenticator 에서 수행했던 로직을 처리 하는 것 입니다.
이를 통해 보다 명시적으로 서버의 응답 코드에 대응해서 처리할 수 있다는 장점이 있습니다.
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(TokenInterceptor(tokenProvider))
// ... 다른 설정 ...
.build()
마찬가지로 Interceptor 도 다음과 같이 등록합니다.
OkHttpClient를 하나만 공유하고, 동일 호스트로 비동기 요청을 동시에 많이 보내는 환경이라면 데드락(또는 무한 대기) 시나리오가 발생할 수 있습니다.
OkHttp는 Dispatcher.maxRequestsPerHost로 호스트당 비동기 요청 동시 실행을 기본 5개로 제한합니다.
(해당 시나리오는 다음 글을 참고했습니다.)
따라서 아래 상황이 가능해집니다.
authenticate()로 들어감authenticate() 내부에서 runBlocking으로 Dispatcher 스레드를 점유enqueue하면, 이미 호스트 슬롯이 꽉 차서 “6번째 요청”이 ready 큐에 쌓인 채 실행을 못 함이런 케이스를 피하려면 다음 중 하나가 필요합니다.
Call.execute()) 기반으로 분리해서 Dispatcher 슬롯 경쟁을 피하기OkHttpClient를 분리해서 메인 요청 Dispatcher와 분리하기제가 진행하고 있는 프로젝트의 경우 해당 부분에 대해서 OkhttpClient 를 분리해서 사용하고 있었기에 따로 동기 기반의 요청으로 구조 변경을 하진 않았습니다.
이번 글에서는 안드로이드 앱의 네트워크 통신에서 필수적인 토큰 만료 처리와 OkHttp를 활용한 갱신 전략을 살펴보았습니다.
단순히 401 Unauthorized에 대응하는 것을 넘어, Authenticator와 Interceptor의 특성을 정확히 이해하고 선택하는 것이 중요합니다. 특히 비동기 요청이 빈번한 환경에서 발생할 수 있는 동시성 이슈(Concurrency Issue)와 네트워크 데드락(Deadlock) 문제는 안정적인 앱 서비스를 위해 반드시 짚고 넘어가야 할 지점입니다.