okhttp Authenticator가 발생하는 과정

동키·2025년 4월 11일

안드로이드

목록 보기
9/14

이전 포스팅에서 okhttp가 가지고 있는 interceptor 에 대해 알아보았습니다.

그 중 RetryAndFollowUpInterceptor 가 서버로부터 401 response를 받을 시 OkHttpClientAuthenticatorauthenticate 를 호출한다고 언급했습니다.

이번 포스팅에서는 어떻게 RetryAndFollowUpInterceptor 이 친구가 이렇게 동작할 수 있는지에 대해 알아보자 합니다.

RetryAndFollowUpInterceptor 코드를 보시면서 보시는 걸 추천드립니다!**

RetryAndFollowUpInterceptor

			while (true) {
73      try {
74        response = realChain.proceed(request)
75        newRoutePlanner = true
76      } catch (e: IOException) {
77        // An attempt to communicate with a server failed. The request may have been sent.
78         if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
79          throw e.withSuppressed(recoveredFailures)
80        } else {
81          recoveredFailures += e
82        }
83        newRoutePlanner = false
84        continue
85     }
		}

우선, 최초의 요청(request)에 대해 realChain.proceed(request)를 호출하여 서버에 요청을 보내고 응답(response)을 받습니다.

응답 후 followUpRequest 함수 호출(227번째 line)

 private fun followUpRequest(
    userResponse: Response,
    exchange: Exchange?,
    call: RealCall,
  ): Request? {
    val responseCode = userResponse.code
    when (responseCode) {
      .
			.
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse).also {
        if (it != null) {
          call.eventListener.retryDecision(call, true, "Received HTTP_UNAUTHORIZED (401) and authenticate request")
        } else {
          call.eventListener.retryDecision(call, false, "Received HTTP_UNAUTHORIZED (401) without authenticate request")
        }
      }

바로 이 코드에서 HTTP_UNAUTHORIZED 일 때

return client.authenticator.authenticate(route, userResponse)

클라이언트의 authenticatorauthenticate 를 호출하는 것을 확인할 수 있습니다.

여깃었구나…ㅠㅠ

자 그럼 authenicate가 무엇을 반환할까요?

저같은 경우 서버로부터 refreshToken을 전달해 accessToken을 새로 받고 다시 요청을 보냈습니다.

  response.request.newBuilder()
                    .header("Authorization", "Bearer ${tokenData.accessToken}")
                    .build()
  // return 타입은 Request? 임

이렇게 말이죠.

다시 RetryAndFollowUpInterceptor 코드로 돌아가보겠습니다.

 val followUp = followUpRequest(response, exchange, call)
  if (followUp == null) {
          if (exchange != null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }
response.body.closeQuietly()
request = followUp
priorResponse = response

authenticate가 null을 반환하지 않고 request 를 반환하니 if(followUp == null) 코드는 패스합니다.

그 후 response.body.closeQuietly()로 이전 응답의 리소스를 정리한 후

request 변수에 새 요청(followUp)을 할당합니다.

priorResponse에 이전 응답을 저장합니다.

while 루프가 다음 반복으로 진입합니다.

while 루프의 반복에서 새로운 request(즉, 인증 정보가 업데이트된 요청)를 가지고 다시 realChain.proceed(request)가 호출됩니다.

이와 같이 while 루프는 새 요청이 계속 생성(즉, followUp이 계속 반환)되면 반복되며, 최종적으로 재시도할 필요가 없을 때 followUp이 null이 되어 최종 응답을 반환합니다.

여기서 또 궁금한게 생겼습니다.

priorResponse에 왜 이전 응답을 저장해주지?

리다이렉션이나 인증 실패와 같이 follow-up 요청이 발생할 때 이전에 받은 응답을 새로운 요청과 연결함으로써, 최종 응답에 이력(response chain)을 포함시키기 위함입니다.

이 이력을 통해 최종 결과가 도출되기까지 어떤 중간 응답이 있었는지를 추적할 수 있으며, 이는 디버깅이나 로깅에 매우 유용합니다

실제로 Authenticator 에서 이를 사용했단 사실!!

class TokenAuthenticator @Inject constructor(
    private val tokenManager: TokenManager,
    private val authService: AuthService,
): Authenticator  {
    override fun authenticate(route: Route?, response: Response): Request? {
        // 무한 루프 방지
        if (responseCount(response) >= 2) return null
        return runBlocking {
           ...
        }
    }

    private fun responseCount(response: Response): Int {
        var count = 1
        var r = response.priorResponse
        while (r != null) {
            count++
            r = r.priorResponse
        }
        return count
    }
}

responseCount 함수에서 응답객체의 response. priorResponse 를 사용하여 무한 요청이 가지 않게 막은 모습을 볼 수 있습니다.

이 과정이 일어나는 도식화로 마무리 하겠습니다!

profile
오늘 하루도 화이팅

0개의 댓글