ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Retrofit / RxJava] 네트워크 요청 결과를 RxJava로 처리하자 -4-
    개발/Android 2019. 4. 9. 23:12
    반응형

    이전글

    2019/03/13 - [개발/Android] - [Retrofit / RxJava] 네트워크 요청 결과를 RxJava로 처리하자 -1-

    2019/04/04 - [개발/Android] - [Retrofit / RxJava] 네트워크 요청 결과를 RxJava로 처리하자 -2-

    2019/04/06 - [개발/Android] - [Retrofit / RxJava] 네트워크 요청 결과를 RxJava로 처리하자 -3-

     

      앞에서 retry()를 이용하여 네트워크 요청 실패시 재시도 하는 방법에 대해서 구현해봤습니다. 재시도를 할때도 그냥 바로 재시도를 하는것이 아니라 특정한 조건을 만족한 뒤 재시도를 해야 할 경우가 있을수 있습니다. 이번에는 서버 인증정보를 갱신 후 재시도를 하는 방법을 추가해보도록 하겠습니다.

     

     ※ 실제 갱신동작을 할 서버API가 없기때문에 몇몇 동작에서는 성공을 가정하고 진행하겠습니다. 실제로 적용할때는 정상적으로 원하는 동작이 이루어지는지 확인 후 적용해야 합니다.

     

     네트워크 요청시 Header에 인증정보를 설정하도록 하겠습니다. URL에 포함되는 경우도 있지만 Header에 추가하는 방식이 더 많이 쓰이는듯 하여 이렇게 진행하겠습니다. Retrofit을 쓰는경우 interface 정의할때 @Header를 통해 설정하는 것도 가능하지만 재시도할때 쉽게 처리 하기위해 Interceptor를 통해 설정하도록 하겠습니다.

    object Authentication {
        var tokenId: String? = null
        
        ...
        
        fun getAuthenticationInterceptor(): Interceptor {
            return Interceptor { chain ->
                val builder = chain.request().newBuilder()            
                builder.addHeader(
                        "Authorization",
                        String.format(Locale.getDefault(), "Bearer %s", tokenId)
                )            
                return@Interceptor chain.proceed(builder.build())
            }
        }
        
        ...
        
    }

     Interceptor를 통해 동적으로 Header에 Authorization 값을 설정합니다. 앞에서 말했지만 API의 parameter로 받아서 설정하는 방법도 있지만 그럴경우 token을 갱신 후 재시도 할때 처음부터 다시 동작하도록 해야 해서 이렇게 설정했습니다. 

     

     이제 Interceptor를 Retrofit 생성시점에 등록해 주도록 합시다. 앞서 GithubRepository에 만들었던 createRetrofit() 메소드는 유틸성 클래스로 옮기도록 합시다. 

    object NetworkHelper {
        @JvmStatic
        fun createRetrofit(baseUrl: String): Retrofit {
            return Retrofit.Builder()
                .client(
                    OkHttpClient.Builder()
                        .connectionPool(ConnectionPool(5, 20, TimeUnit.SECONDS))
                        .addInterceptor(Authentication.getAuthenticationInterceptor())
                        .addInterceptor(HttpLoggingInterceptor(
                            HttpLoggingInterceptor.Logger {
                                Log.i("TEST", it)
                            }).apply {
                            level = if (BuildConfig.DEBUG)
                                HttpLoggingInterceptor.Level.BASIC
                            else
                                HttpLoggingInterceptor.Level.NONE
                        })
                        .build()
                )
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(baseUrl)
                .build()
        }
    }

     addInterceptor()를 통해 AuthenticationInterceptor를 추가 했습니다. 여기에 object 내부의 function에 @JvmStatic을 붙였는데 이렇게 annotation을 붙이는 경우 디컴파일 했을때 기존에 Java Util 클래스에서 사용하던 형태와 비슷하게 static 메소드로 생성됩니다. 굳이 붙이지 않아도 Util 클래스의 메소드 형태상 순수함수로 구성되기 때문에 동기화 문제나 멤버변수 문제가 생기지는 않긴 합니다만 어느쪽이 좋고 나쁜지는 아직은 잘 모르겠습니다. 참고만 하시면 됩니다.

     

     이상태로 요청을 해보도록 하겠습니다. 기존에 추가해 놓은 Follower를 받아오는 Api에 임의로 tokenId를 test로 설정하고 요청하겠습니다.

    I/TEST: --> GET https://api.github.com/users/octocat/followers
    I/TEST: Authorization: Bearer test
    I/TEST: --> END GET
    I/TEST: <-- 401 Unauthorized https://api.github.com/users/octocat/followers (1295ms)
    I/TEST: Date: Tue, 09 Apr 2019 13:05:16 GMT
    
    ...
    
    I/TEST: {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}
    I/TEST: <-- END HTTP (83-byte body)
    I/TEST: --> GET https://api.github.com/users/octocat/followers
    I/TEST: Authorization: Bearer test
    I/TEST: --> END GET
    I/TEST: <-- 401 Unauthorized https://api.github.com/users/octocat/followers (263ms)
    I/TEST: Date: Tue, 09 Apr 2019 13:05:17 GMT
    
    ...
    
    I/TEST: {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}
    I/TEST: <-- END HTTP (83-byte body)
    I/TEST: --> GET https://api.github.com/users/octocat/followers
    I/TEST: Authorization: Bearer test
    I/TEST: --> END GET
    I/TEST: <-- 401 Unauthorized https://api.github.com/users/octocat/followers (621ms)
    I/TEST: Date: Tue, 09 Apr 2019 13:05:17 GMT
    
    ...
    
    I/TEST: {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}
    I/TEST: <-- END HTTP (83-byte body)
    t I/TEST: clickRequest httpexception code : 401 exception.response.code : 401

     401오류가 발생했습니다. 2회 재시도까지 진행되어도 tokenId가 갱신되지 않으면 네트워크 요청이 성공할 수 없습니다. 오류 상황을 체크하여 401일경우는 tokenId를 갱신하는 네트워크를 실행하고 그렇지 않은경우는 2회 재시도를 시도하도록 추가해 보겠습니다.

     이런상황일때 활용할 수 있도록 RxJava에서는 retryWhen()을 제공하고 있습니다. 단, Observable이랑 RxJava2에서 추가된 Flowable, Single, Maybe, Completable과는 다른 파라매터를 가지기 때문에 구현 방식은 조금 달라질 수 있습니다.

     

     retryWhen()을 통해 발생한 Throwable을 체크하여 tokenId를 갱신하는 네트워크를 실행해보도록 하겠습니다. retryWhen()을 통해 반환하는 객체가 Observable같은 반응형 객체이기 때문에 가능한 방식입니다.

    object Authentication {
        
        ...
    
        private fun getTokenRefreshRequest(): Flowable<AuthToken> {
            return authApi.refreshToken("octocat").subscribeOn(Schedulers.io())
                .doOnSuccess {
                    Log.i("TEST", "success refresh token")
                    tokenId = it.tokenId
                }.doOnError { tokenId = null }.retry(2).toFlowable()
        }
        
        fun getRetryWhenWithRefreshToken(): Function<Flowable<Throwable>, Publisher<*>> {
            return RetryFunctionWithTokenRefesh()
        }
    
        private class RetryFunctionWithTokenRefresh : Function<Flowable<Throwable>, Publisher<*>> {
            private var count: Int = 0
            override fun apply(t: Flowable<Throwable>): Flowable<*> {
                count = 0
                return t.flatMap {
                    if (count == 2) {
                        return@flatMap Flowable.error<Throwable>(it)
                    }
                    count += 1
                    return@flatMap if (isUnAuthorizedError(it)) {
                        getTokenRefreshRequest()
                    } else {
                        Flowable.timer(1, TimeUnit.SECONDS)
                    }
                }
            }
    
            private fun isUnAuthorizedError(throwable: Throwable): Boolean {
                if (throwable !is HttpException) {
                    return false
                }
                val code = throwable.code()
                return code == HttpURLConnection.HTTP_UNAUTHORIZED
            }
        }
        
        ...
        
    }

     retryWhen()을 통해 동작할 Function interface를 구현한 클래스를 정의합니다. 내부적으로 retryTimes를 체크 합니다. t.take(retryTimes).flatMap()을 통해 정의 할 수 있지만 이럴경우는 입력한 횟수를 넘어가는 경우 반환되는 오류가 네트워크 요청으로 반환되는 오류가 넘어오지 않아 내부적으로 처리하도록 했습니다. take()를 통해 처리 할 수 있는지는 좀 더 찾아봐야 할듯 합니다.

     발생한 오류가 401오류 인경우 tokenId를 갱신하는 네트워크를 실행하고 아닌경우는 1초 timer를 통해 잠시 대기 후 재시도를 합니다. 미리 정의된 retryTimes만큼 재시도를 한경우 Flowable.error()를 통해 마지막으로 발생한 오류를 반환하는 로직 입니다. 

     

     ※ 위에서 정의된 refreshToken용 api는 실제가 아닌 동작이므로 시도했을 경우 실패하지만 여기서는 성공한것으로 간주하고 tokenId를 갱신했다고 처리 하겠습니다.

     

     이제 네트워크 요청시 설정했던 기존의 retry()를 제거하고 retryWhen()으로 동작하도록 변경해 보도록 하겠습니다. 구현체로 다 만들었기 때문에 메소드를 통해 설정만 해주면 됩니다.

    class GithubRepository {
        
        ...
    
        private fun requestFollowers(username: String): Single<Response<List<GithubUser>>> {
            return githubApi.getFollowers(username).subscribeOn(Schedulers.io())
                .map { t -> if (t.isSuccessful) t else throw HttpException(t) }
                .retryWhen(Authentication.getRetryWhenWithRefreshToken())
                .observeOn(AndroidSchedulers.mainThread())
        }
        
        ...
        
    }

     retryWhen()도 설정했고 이제 다시 테스트 해보도록 하겠습니다. 앞서 요청했던 Follower를 받아오는 Api를 다시 요청해보도록 하겠습니다.

    I/TEST: --> GET https://api.github.com/users/octocat/followers
    I/TEST: Authorization: Bearer test
    I/TEST: --> END GET
    I/TEST: <-- 401 Unauthorized https://api.github.com/users/octocat/followers (355ms)
    I/TEST: Date: Tue, 09 Apr 2019 13:58:02 GMT
    
    ...
    
    I/TEST: {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}
    I/TEST: <-- END HTTP (83-byte body)
    I/TEST: --> GET https://api.github.com/authorizations/octocat
    I/TEST: Authorization: Bearer test
    I/TEST: --> END GET
    I/TEST: <-- 401 Unauthorized https://api.github.com/authorizations/octocat (449ms)
    I/TEST: Date: Tue, 09 Apr 2019 13:58:02 GMT
    
    ...
    
    I/TEST: {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}
    I/TEST: <-- END HTTP (83-byte body)
    I/TEST: success refresh token
    I/TEST: --> GET https://api.github.com/users/octocat/followers
    I/TEST: --> END GET
    I/TEST: <-- 200 OK https://api.github.com/users/octocat/followers (1289ms)
    I/TEST: Date: Tue, 09 Apr 2019 13:58:03 GMT
    
    ...
    
    I/TEST: <-- END HTTP (26729-byte body)
    I/TEST: clickRequestSuccess success 200

     위에서 보면 401이 발생하고 authorizations/를 통해 tokenId 갱신을 시도 합니다. 여기서 똑같이 401이 발생하지만.. 성공했다고 치고 tokenId를 갱신후 다시 follower를 받아오는 Api를 재시도 해서 성공합니다. 지금 이 형태의 네트워크를 retryWhen()으로 자동화 하지 않고 별도의 callback을 이용해서 구현하면 제법 번거로울수 있지만 이렇게 구성해 두면 별도로 신경쓰지 않고 tokenId를 갱신하도록 할 수 있습니다.

     

     지금까지 Retrofit을 이용한 네트워크 요청에 대해서 구현해 봤습니다. 기본적인 방법부터 전처리를 통한 결과값 변환, 오류가 발생했을때 재시도 기능까지 구현을 했고 마지막으로 재시도 할때의 추가적인 동작방안까지 추가해봤습니다.

     retryWhen()을 통한 활용방안은 여기서는 인증만료시 갱신하도록 했지만 좀 더 생각해 보면 특정 오류 발생시 로그를 남긴뒤 재시도 한다거나 하는 등의 다른 활용방안도 있을수 있으니 요구되는 기능에 따라 활용할 수 있도록 여러가지로 고민해 보면 좋을것 같습니다.

    반응형
Designed by Tistory.