이전 포스팅에서 okhttp가 가지고 있는 interceptor 에 대해 알아보았습니다.
그 중 RetryAndFollowUpInterceptor 가 서버로부터 401 response를 받을 시 OkHttpClient 의 Authenticator 의 authenticate 를 호출한다고 언급했습니다.
이번 포스팅에서는 어떻게 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)
클라이언트의 authenticator 의 authenticate 를 호출하는 것을 확인할 수 있습니다.
여깃었구나…ㅠㅠ
자 그럼 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이 되어 최종 응답을 반환합니다.
여기서 또 궁금한게 생겼습니다.
리다이렉션이나 인증 실패와 같이 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 를 사용하여 무한 요청이 가지 않게 막은 모습을 볼 수 있습니다.
이 과정이 일어나는 도식화로 마무리 하겠습니다!
