Add common parameter on Retrofit using Interceptor

도입

Retrofit 의 기반이 되는 OKHttp 에는 Interceptor 란 기능이 있다.

일반적으로 Retrofit 를 사용할 때 Retrofit.create 로 생성한 서비스 객체를 이용해서 요청을 하는데, 그 통신 과정을 잡아서 일정한 행동을 처리하게 하는 것이다.

흔히 로그를 표시할 때 사용하는 Okhttp-logging-interceptor 도 이런 방식을 이용한다.

이 Interceptor 는 두 가지가 있는데, 첫번째는 이번에 사용할 Application Interceptor 이고, 나머지는 Network Interceptor 이다.

Network Interceptor 는 단순히 원격 소스에 대한 Interceptor 를 제공하는 반면 Application Interceptor 는 원격 소스 말고도 Cache 에 대한 Interceptor 도 제공한다.

기본형

Interceptor 를 구현하기 위해서는 Interceptor 라는 클래스를 상속할 필요가 있다.

해당 클래스에는 intercept 라는 메서드를 가지고 있는데, Interceptor.Chain 를 주고서 Response 를 얻는 식이다. 즉, 해당 Interceptor 에서 기존의 Request 를 기반으로 조작해서 새로운 Request 를 제작하는 형태라고 설명할 수 있다.

Interceptor 를 상속하는 클래스를 만들고, 기존의 Request 를 가지고 새 Request 를 만들어서 다시 Response 로 반환하는 코드는 다음과 같다.

class AddParamsInterceptor() : Interceptor {

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val originalHttpUrl = original.url()
        val request = original.newBuilder()
                              .url(originalHttpUrl.newBuilder().build())
                              .build()
        return chain.proceed(request)
    }
}

활용 : 파라미터 추가하기

이번 글에서는 이 Interceptor 를 사용해서 Retrofit 를 거치는 모든 통신에 공통으로 들어가는 파라미터를 넣고자 한다. 즉, GET 에 들어갈 Query Parameter 와 POST 에 들어갈 Field Parameter 를 넣으면, Interceptor 를 통해 기존 파라미터에 파라미터를 추가하는 것이다.

GET 에 들어갈 Query Parameter 의 경우 HttpUrl, 즉 chain.request 의 반환형에서는 addQueryParameter 라는 다소 직관적인 인터페이스를 제공하지만, POST 의 경우에는 RequestBody, 즉 request.body() 의 반환형은 직관적인 인터페이스를 제공하지 않는다. 따라서 이 RequestBody 를 상속하는 또 다른 클래스를 만들어서 그 클래스가 기존 body 위에 새로운 내용을 삽입하도록 해야한다.

우선 Query Parameter 를 추가해보자.

val original = chain.request()
val originalHttpUrl = original.url()
val urlBuilder = originalHttpUrl.newBuilder()

for ((key, value) in mGETMap.entries) {
    urlBuilder.addQueryParameter(key, value)
}

그 다음, Field Parameter 를 추가해보자.

if (original.body() != null) {
    val requestBody = original.body()!!
    val paramList = mPOSTMap.entries.map { "&" + it.key + "=" + it.value }
    val paramLength = paramList.map { it.length }.sum()

    val newRequestBody = object : RequestBody() {
        override fun contentLength(): Long = requestBody.contentLength() + paramLength
        override fun contentType(): MediaType? = requestBody.contentType()

        @Throws(IOException::class)
        override fun writeTo(sink: BufferedSink) {
            requestBody.writeTo(sink)
            for (param in paramList) {
                sink.writeString(param, Charset.forName("UTF-8"))
            }
        }
    }

    requestBuilder.post(newRequestBody)
}

Query Parameter 와는 다르게 직접 query string 를 만들고, contentLength 등도 직접 다 추가해야 한다.

완성

파라미터를 추가하는 Interceptor 를 쉽게 사용할 수 있게 Builder 등 코드를 더 추가한 것이 다음과 같다.

class AddParamsInterceptor(getMap: HashMap<String, String>, postMap: HashMap<String, String>) : Interceptor {
    private var mGETMap = getMap
    private var mPOSTMap = postMap

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val originalHttpUrl = original.url()
        val urlBuilder = originalHttpUrl.newBuilder()
        val requestBuilder = original.newBuilder()

        for ((key, value) in mGETMap.entries) {
            urlBuilder.addQueryParameter(key, value)
        }

        val httpUrl = urlBuilder.build()

        if (original.body() != null) {
            val requestBody = original.body()!!
            val paramList = mPOSTMap.entries.map { "&" + it.key + "=" + it.value }
            val paramLength = paramList.map { it.length }.sum()

            val newRequestBody = object : RequestBody() {
                override fun contentLength(): Long = requestBody.contentLength() + paramLength
                override fun contentType(): MediaType? = requestBody.contentType()

                @Throws(IOException::class)
                override fun writeTo(sink: BufferedSink) {
                    requestBody.writeTo(sink)
                    for (param in paramList) {
                        sink.writeString(param, Charset.forName("UTF-8"))
                    }
                }
            }

            requestBuilder.post(newRequestBody)
        }

        requestBuilder.url(httpUrl)

        val request = requestBuilder.build()
        return chain.proceed(request)
    }


    class Builder {
        private val mGETMap = hashMapOf<String, String>()
        private val mPOSTMap = hashMapOf<String, String>()

        /**
         * add (key, value) into additional common parameter with @Query annotation
         */
        fun addQueryParameter(key: String, value: String) = this.apply { mGETMap[key] = value }

        /**
         * add (key, value) into additional common parameter with @Field annotation
         */
        fun addFieldParameter(key: String, value: String) = this.apply { mPOSTMap[key] = value }

        /**
         * add (key, value) into additional common parameter with @Query, @Field annotation
         */
        fun addParameter(key: String, value: String) = this.apply { addQueryParameter(key, value).addFieldParameter(key, value) }

        /**
         * Building AddParamsInterceptor
         */
        fun build() = AddParamsInterceptor(mGETMap, mPOSTMap)
    }
}

실제 사용하는 방법은 다음과 같다.

@Provides
public OkHttpClient provideClient(Interceptor interceptor) {
    OkHttpClient.Builder builder = new OkHttpClient().newBuilder();
    builder.readTimeout(Config.getConfig().getTimeout(), TimeUnit.MILLISECONDS);
    builder.connectTimeout(Config.getConfig().getConnectTimeout(), TimeUnit.MILLISECONDS);
    builder.addInterceptor(interceptor);

    AddParamsInterceptor addParamsInterceptor = new AddParamsInterceptor.Builder()
            .addParameter("key", "abc")
            .build();
        
    builder.addInterceptor(addParamsInterceptor);
        
    return builder.build();
}