본문 바로가기

개발일기

Retrofit! Eureka!

Fragment랑 같이 쓰려다가 MainFragment 예시 때문에 글이 너무 길어져서.

 

우선 response가 제대로 돌아오지 않던 이유를 찾았다. RetrofitGson을 디폴트로 사용하기 때문에, 내가 Body로 전송하는 인스턴스가 Json으로 자동으로 serialized 되었기 때문.

 

즉, 이미 serialized 된 JSONObject를 한 번 더 toJson() 시켜주는 삽질을 하고 있던 거다.

 

하여, 다시 만든 인터페이스.

LogInApi.kt

interface LogInApi {

    @POST("/Login")
    fun startLogin(@Body jsonObj: Operator) : Call<ApiResult>

    companion object {
        fun create() : ApiInterface {
            val retrofit = Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(StaticValue.baseUrl)
                .build()

            return retrofit.create(LogInApi::class.java)
        }
    }
}

 

ActivityLogIn.kt

private fun startLogIn(objInfo: Obj) {
        val apiInterface = LogInApi.create().startLogin(opInfo)
        apiInterface.enqueue(object : Callback<ApiResult> {
            override fun onResponse(call: Call<ApiResult>, response: Response<ApiResult>?) {
                // 여기서 디버깅 포인트 찍어서 data 관측 중
            }

            override fun onFailure(call: Call<JsonObjBase>, t: Throwable) {
                TODO("Not yet implemented")
            }
        })
}

ApiResult의 "Data"가 JSONObject 형태로 들어올 때를 우선 가정해서 (이걸 넘겨야 그 다음을 하니까) 아래와 같이 만들었었다.

data class JsonObjBase (
  @SerializedName("ResultCode") val resultCode : Int,
  @SerializedName("ResultValue") val resultValue : String,
  @SerializedName("ResultMessage") val resultMessage : String,
  @SerializedName("Data") val data : JSONObject
)

 

문제는 Data의 정상적인 파싱 및 DTO로의 이식이 안 된 것.

 

StackoverFlow에 질문도 올리고 검색도 이리저리 해보다가 결국 Any를 사용하기로 했다. 그래, 어차피 "Data"는 문자로도 JSONObject로도 JSONArray로도 들어오는데 다 대응해야지... 하는 김에 제대로 해야지 하고. (아님)

 

하여 완성판(?) ApiResult

ApiResult.kt

data class ApiResult(

    @field:SerializedName("Data")
    val data: Any? = null,

    @field:SerializedName("ResultValue")
    val resultValue: String? = null,

    @field:SerializedName("ResultMessage")
    val resultMessage: String? = null,

    @field:SerializedName("ResultCode")
    val resultCode: Int? = null
)

 

"Data"로 들어오는 모든 값들의 key들을 넣어두고 DTO에서는 try/catch를 활용하여 자기 것만 취하는 식으로 구조를 짰다. 더 좋은 방법이 있을 텐데... 당장은 떠오르지가 않는다. ㅠㅠ

ResponseData.kt

data class ResponseData(

    @field:SerializedName("UserName")
    val userName: String? = null,

    @field:SerializedName("Enabled")
    val enabled: Int? = null,

    @field:SerializedName("UserId")
    val userId: String? = null,


    @field:SerializedName("Permission")
    val permission: Long? = null,

    ...
)

 

이제는 "Data"를 상황에 맞추어 제대로 가져올 수 있다. 적어도 위에서 말한 세 가지 케이스에는 모두 성공한다!

 

다만 주의할 점은 "Data"의 JSONArray elements를 asJsonObject로 변환하면 JSONObject가 아니라 JsonObject로 바뀐다. (아니 둘 차이 처음에 알지도 못했어요 코드로 잘 안 보여서 망할 case sensitive)

 

따라서 Data Class에서 이를 변환시켜줄 때는 JSONObj[foo] as String 이런 식이 아니라 JSONObj[foo].asString으로 받아야 한다는 걸 기억해야 한다.

 

각각의 case에 대한 샘플 코드

// JSONObject
val type = object : TypeToken<MyClass>(){}.type
val g = Gson()
val result = g.toJson(response.body()!!.data)
val resultMyClass = MyClass(JSONObject(result))

// JSONArray
val type = object : TypeToken<MutableList<MyClass2>>(){}.type
val g = Gson()
val result = g.toJsonTree(response.body()!!.data, type)
val resultMyClass2 = result.asJsonArray

val myClass2List = ArrayList<MyClass2>()
if (resultMyClass2.size() > 0) {
    for (i in 0 until resultMyClass2.size()) {
        myClass2List.add(MyClass2(resultMyClass2[i].asJsonObject)
    }
}

// String
val g = Gson()
val result = g.toJson(response.body()!!.data)
val resultStr = result.toString()