모든 요청 · 응답을 가로채 공통 처리를 한 곳에 모은다.
class AuthInterceptor(private val store: TokenStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${store.accessToken}")
.build()
return chain.proceed(request)
}
}
OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(store))
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) BODY else NONE // ★ 릴리스에서 끈다
})
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
두 종류
-
addInterceptor — 애플리케이션 인터셉터
- 한 번만 불린다. 리다이렉트·재시도를 신경 안 써도 된다
- 캐시에서 응답이 와도 불린다
-
addNetworkInterceptor — 네트워크 인터셉터
- 실제 네트워크 요청마다 불린다 (리다이렉트마다)
- 캐시 히트면 안 불린다
인증 헤더 · 로깅 → 애플리케이션 인터셉터 실제 전송 바이트 관찰 → 네트워크 인터셉터
토큰 갱신은 Authenticator로
class TokenAuthenticator(private val store: TokenStore) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (response.responseCount >= 2) return null // ★ 무한 루프 방지
val fresh = runBlocking { store.refresh() } ?: return null
return response.request.newBuilder()
.header("Authorization", "Bearer $fresh").build()
}
}
왜 인터셉터가 아니라 Authenticator 인가
- OkHttp 가 401 응답을 받았을 때만 호출한다
- 갱신 시도를 401 에만 한정할 수 있다
주의
- 반드시 재시도 횟수를 제한한다 (안 하면 무한 401 루프)
- 동시 요청 여러 개가 동시에 갱신하지 않도록 뮤텍스로 묶는다
로깅을 릴리스에 남기면
BODY 레벨은 요청/응답 본문을 전부 Logcat 에 찍는다
- 토큰 · 개인정보가 로그로 새어 나간다
- adb logcat 으로 누구나 읽을 수 있다
BuildConfig.DEBUG 분기는 R8 이 릴리스에서 통째로 지운다
캐시
.cache(Cache(File(ctx.cacheDir, "http"), 10L * 1024 * 1024))
OkHttp 는 HTTP 캐시 헤더를 그대로 따른다 서버가 Cache-Control 을 안 주면 캐시되지 않는다
- 필요하면 네트워크 인터셉터에서 응답 헤더를 덧붙인다 (신중하게)
면접 함정
- ❌ 인터셉터에서 토큰을 갱신한다 → 모든 요청마다 검사가 돌고 동시성 처리가 복잡해진다. 401 기반이 자연스럽다.
- ❌
OkHttpClient를 요청마다 새로 만든다 → 커넥션 풀과 캐시가 재사용되지 않는다. 싱글턴으로 둔다.