마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

레트로핏 인터셉터 및 토큰 재발급

0 추천

현재 레트로핏을 사용중이고 에러코드가 401 토큰이 만료되었을때 

 onresponse{ 
      if (response.isSuccessful) {
      } else {
          if(response.code() == 401) {
             // 토큰 재발급 및 재발급성공시 API 재호출
         }
     }

현재 주석되어있는 부분에 각 API 마다 저 메소드들을 써줬었는데 이곳저곳 물어보니

인터셉터를 사용하라고 하더라구요

class LogInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var response = chain.proceed(request)
        Log.e("tag", "request? " + request.method)
        Log.e("tag", "response code? " + response.code)
        Log.e("tag", "response url? " + response.request.url)
        Log.e("tag", "response header? " + response.request.headers)
        
        return response
    }
}

그래서 이렇게 써서 해당 API response code랑 호출하는 풀 URL을 들고오는건 성공했는데

여기서 API를 재호출을 어떻게 해야할지가 막혔습니다 토큰만료되서 리프레시하는거야 공통코드니까 

쓰면된다지만 A액티비티에선 aa라는 API를 호출하고, B액티비티에선 bb라는 API를 호출하는데

이경우는 어떻게 해야하는건지 도와주세요..

수원통학러 (3,570 포인트) 님이 2021년 4월 24일 질문

1개의 답변

0 추천

개인적으로는 토큰이 expired되었는데 재발급을 해주는 것은 안전하지 않다는 생각이구요. 토큰이 expired 되기 전에 refresh token 을 보내면 재발급 해주는 것이 안전하다고 생각합니다.

어쨋든, 해당 기능의 구현은 OkHttp에 Authenticator라는게 있는데 이걸 이용하라고 되어 있더라구요. 하지만 여기에는 의견이 갈라지는 듯 합니다. Authenticator가 추가적인 API  호출을 하는 듯합니다. 개인적으로는 안사용하고 있습니다.

인터셉터를 사용해서 이런 식으로 구현이 가능할 것 같습니다. (테스트는 안해봤습니다.)

interface Observervable<T> {
   fun registerObserver(observer: T)
   fun unregisterObserver(observer: T)
}

abstract class Observervable<T>: Observervable<T> {
    
    protected val observers = hashSetOf<T>()  
   
    override  fun registerObserver(observer: T) {
        observers.add(observer)
    }

   override fun unregisterObserver(observer: T) { 
       observers.remove(observer)
   }
}

class TokenExpirationObserver {
    fun onTokenExpired()
}

class LogInterceptor : Interceptor, BaseObservable<TokenExpirationObserver> {

    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var response = chain.proceed(request)
        Log.e("tag", "request? " + request.method)
        Log.e("tag", "response code? " + response.code)
        Log.e("tag", "response url? " + response.request.url)
        Log.e("tag", "response header? " + response.request.headers)

        if (tokenExpired) notifyTokenExpired()
        
        return response
    }

    private fun notifyTokenExpired() {
        for (observer in observers) {
            observer.onTokenExpired()
        }
    }
}

LoginInterceptor 를 Observer 패턴을 사용해서 Observable 로 만듭니다. 그리고 토큰이 expired 되었을 때 Observer 들에게 알려줍니다. LoginInterceptor는 Singleton 으로 만드신 다음 인스턴스를 공유해서 사용하면 될 것 같습니다.

이걸 사용하는 쪽에서는 observer로 등록하고 이벤트가 오면 처리하면 되겠죠. 전 ViewModel을 예로 들었지만, 다른 클래스들에서도 같은 방법으로 사용할 수 있을 것 같습니다.

class MainViewModel(
     private val tokenExpirationObservable: LoginInterceptor
): ViewModel, TokenExpirationObserver {
     override fun onTokenExpired() {
          requestAccessTokenRenewal()
     }

    
    fun onStart() {
        tokenExpirationObservable.registerObserver(this)
    }

    fun onStop() {
        tokenExpirationObservable.unregisterObserver(this)
    }

}

spark (226,420 포인트) 님이 2021년 4월 30일 답변
spark님이 2021년 4월 30일 수정
위의 방법에 덧불여 좀 더 심플한 방법을 생각해 봤습니다. 현재 제가 사용하고 있는 것과 비슷한데요.

Retrofit 서비스를 직접 호출하지 마시고 Proxy나 delegate 클래스를 만드셔서 이 클래스를 이용해서 API를 호출하세요. 그리고 위의 AcessToken을 체크하는 로직을 여기에 넣으세요. HTTP 401 에러를 그대로 사용하실경우라면 LoginInterceptor를 굳이 사용하지 않으셔도 되고 LoginInterceptor 사용하시는 경우이면 토근관련 에러를 던지시고 이걸 catch 해서 사용하세요.
// ApiService 는 님이 사용하시는 Retrofit interface 임.

sealed class Result<T> {
    class Success(val data: T): Result<T>()
    object Failure(val error: Throwable): Result<Nothing>()
}

interface ResposneHandler {
    
     fun <T> onResponse(ressponse: Response<T>): Result<T>
}

class ExpireTokenRefreshHandler:  ResposneHandler {
     // 구현 클래스에서 token expiration에 대한 처리를 해 줌.
}

// ExpireTokenRefreshHandler를 사용
class ApiProxy(
    private val apiService: ApiService,
     private val responseHandler: ResponseHandler
) {
      suspend fun invoke() {
             val response = apiService::apiFunction()
             when (val result = responseHandler.onResponse(response)) {
                  is Result.Success -> handleSuccess()
                  is Result.Failure -> hanldeFailure()
            }
      }

      private fun handleFailure(th: Throwable) {
           
      }
}

API 호출이 필요한 클래스는 ApiProxy를 통해 처리하면 될 것 같아요.
그리고 자바에 보시면 실제로 Proxy 라는 것이 있습니다. A 함수를 호출하면 필요한 동작을 하도록 Invocation handler 라는 것을 통해 처리할 수도 있습니다. 하지만 위의 방식으로도 처리가 가능할 것으로 보이네요.
...