Kotlin에서 reflection을 사용하여 JSON serialization 해보기

JhoonP·2023년 2월 24일
0

Kotlin에서는 JSON serialization, deserialization을 간편하게 할 수 있는 여러 라이브러리들이 존재한다

하지만 외부 라이브러리를 사용하지 않고 Kotlin의 reflection을 사용하여 직접 구현하여 내부 동작을 이해해보고자 한다.

kotlin.serialization은 reflection을 사용하지 않고 구현했다고는 하지만, 여기서는 reflection을 사용해 구현해보고자 한다.

Serialization

먼저 serialization할 object의 property(memeber)들을 알아내야 한다.

//  Filter out any properties that are not public
val properties = target::class.memberProperties.filter { it.visibility == KVisibility.PUBLIC }

reflection에서는 runtime에 KClass::memberProperties으로 property 정보를 가져올 수 있다. property 중에서 public 한정자로 선언된 값만 filtering하여 가져온다.

이후 가져온 property들마다 변환하고자 하는 object에서 값을 추출하여 JSONObject에 넣어주면 된다. 일단 현재 예제에서는 null value는 배제하도록 한다.

fun toJson(target: Any, ignoreNull: Boolean = true): JSONObject {
        //  Filter out any properties that are not public
        val properties = target::class.memberProperties.filter { it.visibility == KVisibility.PUBLIC }
        val json = JSONObject()
        for(prop in properties) {
            val value = prop.getter.call(target)
            if(value != null) {
                json.put(prop.name, value)
            }
        }
        return json
    }
}

이제 해당 코드를 사용해서 아래 class type instance에 serialization을 수행해보자.

data class Person(
    val name: String,
    val age: Int,
    val height: Double,
    val workplace: Company,
    val subWorkplace: List<Company>,
)

data class Company(
    val name: String,
    val isLargeEnterprise: Boolean,
    val numOfEmployees: Int?
)

val workplace = Company(name = "Google", isLargeEnterprise = true, numOfEmployees = null)
val person = Person(
    name = "John Doe",
    age = 30,
    height = 171.2,
    workplace = workplace,
    subWorkplace = listOf(workplace, workplace),
)
val result = JJson().toJson(person).toString(1)
println("result $result")
I/System.out: result {
I/System.out:  "age": 30,
I/System.out:  "height": 171.2,
I/System.out:  "name": "John Doe",
I/System.out:  "subWorkplace": "[Company(name=Google, isLargeEnterprise=true, numOfEmployees=null)]",
I/System.out:  "workplace": "Company(name=Google, isLargeEnterprise=true, numOfEmployees=null)"
I/System.out: }

예상은 했지만 역시나 문제가 많다. 현재는 primitive type(String, Number, Boolean) 외의 값은 serialization 하지 못하고 있다.

property가 Object type임을 감지하고 추가로 serialization 처리해줄 필요가 있다.

fun toJson(target: Any, ignoreNull: Boolean = true): JSONObject {
        //  Filter out any properties that are not public
        val properties = target::class.memberProperties.filter { it.visibility == KVisibility.PUBLIC }
        val json = JSONObject()
        for(prop in properties) {
            val value = prop.getter.call(target)
            if(value != null) {
                when {
                    !prop.returnType.isSubtypeOf(Number::class.createType(nullable = true))
                            && !prop.returnType.isSubtypeOf(Boolean::class.createType(nullable = true))
                            && !prop.returnType.isSubtypeOf(String::class.createType(nullable = true)) -> {
                        json.put(prop.name, toJson(value))
                    }
                    else -> {
                        json.put(prop.name, value)
                    }
                }
            }
        }
        return json
    }

property로 type check를 수행하여 primitive가 아닐 경우, 해당 값에 대해 재귀적으로 serialization을 수행하여 그 결과값을 JSONObject에 저장한다.

I/System.out: result {
I/System.out:  "age": 30,
I/System.out:  "height": 171.2,
I/System.out:  "name": "John Doe",
I/System.out:  "subWorkplace": {
I/System.out:   "size": 1
I/System.out:  },
I/System.out:  "workplace": {
I/System.out:   "isLargeEnterprise": true,
I/System.out:   "name": "Google"
I/System.out:  }
I/System.out: }

Workplace가 성공적으로 serialization 되었다. 하지만 Array 값에서 문제가 생긴다.
Array item들을 serialization하여 Array 형태로 만든 것이 아닌 Array 그 자체를 serialization 해버려서 size라는 Array의 property를 볼 수 있다.

fun toJson(target: Any, ignoreNull: Boolean = true): JSONObject {
        //  Filter out any properties that are not public
        val properties = target::class.memberProperties.filter { it.visibility == KVisibility.PUBLIC }
        val json = JSONObject()
        for(prop in properties) {
            val value = prop.getter.call(target)
            if(value != null) {
                when {
                    prop.returnType.isSubtypeOf(List::class.createType(arguments = listOf(KTypeProjection.STAR), nullable = true)) -> {
                        val jsonArray = JSONArray()
                        (value as List<*>).forEach {
                            it?.let {
                                jsonArray.put(toJson3(it))
                            }
                        }
                        json.put(prop.name, jsonArray)
                    }
                    prop.returnType.isSubtypeOf(Array::class.createType(arguments = listOf(KTypeProjection.STAR), nullable = true)) -> {
                        val jsonArray = JSONArray()
                        (value as Array<*>).forEach {
                            it?.let {
                                jsonArray.put(toJson3(it))
                            }
                        }
                        json.put(prop.name, jsonArray)
                    }
                    !prop.returnType.isSubtypeOf(Number::class.createType(nullable = true))
                            && !prop.returnType.isSubtypeOf(Boolean::class.createType(nullable = true))
                            && !prop.returnType.isSubtypeOf(String::class.createType(nullable = true)) -> {
                        json.put(prop.name, toJson(value))
                    }
                    else -> {
                        json.put(prop.name, value)
                    }
                }
            }
        }
        return json
    }

Array 속성을 가지는 type일(kotlin.Array, kotlin.collectionsList) 경우 내부 item들을 꺼내 JSONArray에 저장하여 JSONObject에 저장하면 된다.

I/System.out: result {
I/System.out:  "age": 30,
I/System.out:  "height": 171.2,
I/System.out:  "name": "John Doe",
I/System.out:  "subWorkplace": [
I/System.out:   {
I/System.out:    "isLargeEnterprise": true,
I/System.out:    "name": "Google"
I/System.out:   }
I/System.out:  ],
I/System.out:  "workplace": {
I/System.out:   "isLargeEnterprise": true,
I/System.out:   "name": "Google"
I/System.out:  }
I/System.out: }

성공적으로 serializtion 된 것을 확인할 수 있다.

마치며


해당 문서는 ChatGPT의 도움을 받아 작성하였습니다.
원래 구글링 해가면서 구현한 내용 바탕으로 글을 작성하려 했는데 혹시나 ChatGPT한테 물어보니 더 보완된 방법을 제시해줘서 글을 엎고 다시 써야 했다...
물론 컴파일 조차 안되거나 문제가 있는 코드를 자주 던져줘서 수정 작업이 들어가야 하는 점이 있었지만 어떤 library, function들로 문제를 접근할 수 있는지 먼저 제시해주고 들어가는 것 만으로도 너무 좋았다. 용도에 맞게 잘쓰면 정말 생산성 올라갈듯...

profile
배울게 끝이 없네 끝이 없어

0개의 댓글