공공 데이터 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 |
Comment