Retrofit Interceptor 로 response 타입을 원하는 대로 바꿔보자

공공 데이터 API 를 연동해서 어플리케이션을 개발하고 있었다. 

 

공공 데이터 API 의 json 반환값은 다음과 같다.

 

{
response:{
    header:{ 
         resultCode: String,
         resultMsg: String,
         type: String,
    }
    body:{
        items:[
            { mrhstNm: String,
              mrhstCode: Integer,
              ctprvnNm: String,
              ...
            }]
    }
    totalCount:Integer,
    numOfRows:Integer,
    pageNo:Integer
    }
}

여기서 나는 유의미한 데이터인 items 만 결과값으로 반환받고 싶었다. 만약 GsonConverterFactory 로 위 response 에서 바로 items 를 추출할려면, response 데이터 클래스를 생성해서 감싼 뒤에 body 클래스를 생성해서 감싼 뒤에 items를 최종적으로 얻어낼 수 있을 것이다. 

내가 필요한 건 리스트인데, 이것을 얻기 위해 불필요한 데이터 클래스를 생성한 뒤에야 결과를 얻을 수 있다. 

 

여기서 데이터 리스트인 items 에 해당하는 값만 얻을 수 있는데, 바로 Interceptor 를 활용하는 것이다.

 

Interceptor란?


Retrofit 은 내부적으로 OkHttp 라는 Http 통신 라이브러리를 사용한다. OkHttp 를 사용한 Http 통신은 다음 그림과 같이 나타낼 수 있다.

Application 부분에서 서버에 요청을 보내면, OkHttp core 에서 해당 요청을 서버로 전송하고, 결과도 OkHttp Core 를 통해서 Application 에 넘어간다. 이 때 Interceptor 는 Application 과 OkHttp 코어 혹은 OkHttp 코어와 서버사이의 요청과 응답을 가로채는 역할을 한다.

 

즉 Interceptor 로 요청의 응답을 변형시켜서 받거나, 요청 자체를 변형해서 보낼 수 있는 것이다. Interceptor 는 인터페이스로 해당 인터페이스를 구현하는 클래스를 생성하고, intercept 라는 함수를 구현하면 된다. 

 

 

//Interceptor 인터페이스를 구현하는 클래스
val interceptor = ResponseInterceptor()
//Interceptor 를 사용하는 새로운 OkHttpClient 생성
val client =
    OkHttpClient.Builder().addInterceptor(interceptor = interceptor).build()
cardRemoteSource = Retrofit.Builder()
    .baseUrl(URLConfig.FACILITY_BASE_URL)
    .client(client) //<- Interceptor 를 사용하는 클라이언트 지정
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    .create(CardAfService::class.java)

 

Retrofit Service 를 생성할 때, 인터셉터를 사용하는 OkHttp  클라이언트를 생성해서 Retrofit 에 넘겨주도록 하자. 그러면 해당 서비스의 모든 요청과 결과는 Interceptor 를 거치게 된다.

 

Interceptor 구현


이제 Interceptor 클래스를 구현해서, response 에서 원하는 데이터만 추출해보도록 하자

 

우선 Interceptor 인터페이스를 구현하는 클래스를 생성하자

 

class ResponseInterceptor : Interceptor {
	
    override fun intercept(chain: Interceptor.Chain) : Response {
    
    }
}

우리는 intercept 라는 함수만 구현해주면 된다. 이 함수에서 요청을 가로채고, 결과를 가로챌 수 있는 것이다. 

 

우선 요청을 가로챘지만, 요청에 딱히 건드릴 부분은 없기 때문에 그대로 요청을 진행해준다.

 

 override fun intercept(chain: Interceptor.Chain): Response {
        //네트워크 요청을 가로챘지만, Response 를 파싱해야 하기 때문에 그대로 요청을 진행함
        val response = chain.proceed(chain.request())
		...

요청을 진행하게 되면, 해당 요청의 원래 결과인 Response 가 반환된다. 이제 response를 json 객체로 만들어서, items 에 해당하는 부분만 빼주면 된다. 

 

우선 최상위 키값인 response에 해당하는 json 객체들을 얻어오자

 

private fun extractItems(responseBody : Response) : JSONObject { 
	val response = JSONObject(responseBody.body!!.string())[KEY_TOP].toString()
 	...
}

companion object {
    private const val EMPTY_JSON = "{}"
    private const val EMPTY_LIST = "[]"
    private const val KEY_TOP = "response"
    private const val KEY_BODY = "body"
    private const val KEY_DATA = "items"
}

이제 body 키 값에 해당하는 Json 객체를 얻어오도록 하자

 

private fun extractItems(responseBody : Response) : JSONObject { 
	val response = JSONObject(responseBody.body!!.string())[KEY_TOP].toString()
    	val body = JSONObject(response)[KEY_BODY].toString()
 	...
}

companion object {
    private const val EMPTY_JSON = "{}"
    private const val EMPTY_LIST = "[]"
    private const val KEY_TOP = "response"
    private const val KEY_BODY = "body"
    private const val KEY_DATA = "items"
}

 

이제 body에 해당하는 JSONObject 를 생성해서 이것을 반환하도록 하자. 그러면 이 Json 객체는 다음 형태가 된다.

 

{
    items:[
        { mrhstNm: String,
          mrhstCode: Integer,
          ctprvnNm: String,
          ...
        }]
}

 

이제 마지막으로 위 json 데이터에서 items 에 해당하는 json 배열을 추출하면 된다.

 

override fun intercept(chain: Interceptor.Chain): Response {
        //네트워크 요청을 가로챘지만, Response 를 파싱해야 하기 때문에 그대로 요청을 진행함
        val response = chain.proceed(chain.request())
        //response 로 부터 items 영역을 추출한다.
        val responseJson = extractItems(responseBody = response)
        //items 에 해당하는 json 배열을 가져온다
        val data = if (responseJson.has(KEY_DATA)) responseJson[KEY_DATA] else EMPTY_LIST
        ...
}

이제 필요한 items json 배열을 추출했다. 새로운 Response 객체에 위 data 를 넘기고, 이 response 객체를 반환하면 모든 통신이 끝이난다.

 

override fun intercept(chain: Interceptor.Chain): Response {
    //네트워크 요청을 가로챘지만, Response 를 파싱해야 하기 때문에 그대로 요청을 진행함
    val response = chain.proceed(chain.request())
    //response 로 부터 items 영역을 추출한다.
    val responseJson = extractItems(responseBody = response)
    //items 에 해당하는 json 배열을 가져온다
    val data = if (responseJson.has(KEY_DATA)) responseJson[KEY_DATA] else EMPTY_LIST
    return response.newBuilder()
        .message(response.message)
        .body(data.toString().toResponseBody())
        .build()
}

이제 위 Interceptor 를 추가한 Retrofit 서비스로, 중요한 데이터인 items 만 바로 뺴오는 HTTP 메소드를 작성할 수 있다.

 

@GET(URLConfig.CARD_AFFILIATION)
suspend fun getCardAffiliation(
    @Query("serviceKey") serviceKey: String = "...",
    @Query("pageNo") pageNumber: Int = 1,
    @Query("numOfRows") resultCount: Int,
    @Query("type") outputType: String,
    @Query("ctprvnNm") cityName: String,
    @Query("signguNm") signguName: String,
    @Query("rdnmadr") roadAddr: String?,
    @Query("latitude") latitude: String?,
    @Query("longitude") longitude: String?
): List<CardAffiliation>

 

 

전체 코드


/**
 * 공공 데이터 response 형태
 * header{
 *  resultCode
 *  resultMsg
 *  type
 * }
 * body{
 *  items <- items 만 빼내는 인터셉터 구현
 * }
 * totalcount
 * numOfRows
 * pageNo
 */
class ResponseInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        //네트워크 요청을 가로챘지만, Response 를 파싱해야 하기 때문에 그대로 요청을 진행함
        val response = chain.proceed(chain.request())
        //response 로 부터 items 영역을 추출한다.
        val responseJson = extractItems(responseBody = response)
        //items 에 해당하는 json 배열을 가져온다
        val data = if (responseJson.has(KEY_DATA)) responseJson[KEY_DATA] else EMPTY_LIST
        return response.newBuilder()
            .message(response.message)
            .body(data.toString().toResponseBody())
            .build()
    }

    private fun extractItems(responseBody: Response): JSONObject {
        //response 의 body 가 null 이면 EMPTY JSON OBJECT 반환
        return if (null == responseBody.body) {
            JSONObject(EMPTY_JSON)
        } else {
            try {
                val response = JSONObject(responseBody.body!!.string())[KEY_TOP].toString()
                val body = JSONObject(response)[KEY_BODY].toString()
                JSONObject(body)
            } catch (e: Exception) {
                JSONObject(EMPTY_JSON)
            }
        }
    }

    companion object {
        private const val EMPTY_JSON = "{}"
        private const val EMPTY_LIST = "[]"
        private const val KEY_TOP = "response"
        private const val KEY_BODY = "body"
        private const val KEY_DATA = "items"
    }

}

'안드로이드' 카테고리의 다른 글

Jetpack Compose State  (0) 2022.01.25
Jetpack Compose Staggered Grid Layout  (0) 2022.01.23