Kotlin에서 JSONObject .get*의 확장 함수를 만들어봤다. (reflection, generics, reified)

2020. 9. 7. 09:33Programming/Kotlin

반응형

반복 구문의 발생

Java, Kotlin에서 JSONObject을 사용하여 JSON으로 작성된 데이터 구조를 파싱할 때, getInt, getString 등을 사용하는 경우가 많았다. 이 메서드는 문자열 키를 받아서 매칭되는 값을 반환하는데, 매칭되는 값이 없을 경우 Exception을 던지게 되어있다. Exception이 발생한 경우에는 null을 반환하도록 코드를 작성하면, 아래와 같다.

try {
  JSONObject.getInt("key")
} catch (JSONException e) {
  e.printStackTrace()
  null
}

try {
  JSONObject.getString("key")
} catch (JSONException e) {
  e.printStackTrace()
  null
}

위에서 JSONObject.getString(), JSONObject.getInt()를 호출하는 구문 중 try, catch문은 타입을 제외하면 동일한 코드이다. 그렇다면 이를 어떻게 하나의 코드로 처리할 수 있을까? 동일한 함수에서 다양한 타입을 처리하기 위해 제네릭을 사용한다는 것은, 익히 알려진 사실이다. 제네릭을 사용하여 코드를 수정해보자.

제네릭의 사용

제네릭을 사용하는 방법은 간단하다. fun 키워드 뒤에 <T>처럼, 타입으로 사용할 대문자를 붙여주기만 하면 된다.

클래스 리플렉션의 사용

The most basic reflection feature is getting the runtime reference to a Kotlin class. To obtain the reference to a statically known Kotlin class, you can use the class literal syntax:

val c = MyClass::class

The reference is a value of type KClass.

Note that a Kotlin class reference is not the same as a Java class reference. To obtain a Java class reference, use the .java property on a KClass instance.

공식 문서에 나와있는 클래스 리플렉션에 대한 정의이다. 간단하게 요약하면 코틀린 클래스의 런타임 레퍼런스를 값으로 가져온다는 얘긴데, 보다 자세한 내용을 확인하려면 공식문서를 참조하도록 하자.

private fun <T> JSONObject.tryGet(key: String, type: Class<T>): T? = try {
    when (type) {
      Int::class -> getInt(key) as T
      Boolean::class -> getBoolean(key) as T
      String::class -> getString(key) as T
      JSONArray::class -> getJSONArray(key) as T
      Double::class -> getDouble(key) as T
      else -> null
    }
  } catch (e: Exception) {
    null
  }

여기서 JSONObject.tryGet에서는 함수의 바디에서 T의 타입을 사용하기 위해, 명시적으로 type:Class<T>라는 파라메터를 전달하고 있다. 그 뒤, 리플렉션을 사용하여 Int, Boolean, String, JSONArray, Double의 타입과 type:Class<T>를 비교하여 분기처리를 하고 있다.

reified의 사용

reified에 대한 자세한 내용은 (reified type parameter)[https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters]을 참고하도록 하자.

위에서 제네릭 함수 tryGet을 작성할 때, 런타임에서는 T의 타입을 접근할 수 없기 때문에 type:Class<T>와 같이 명시적으로 타입을 전달했다. (컴파일 타임에는 존재하지만, 런타임에는 Type erasure때문에 접근할 수 없기 때문이다. 이는 코틀린에서 reified는 왜 쓸까?
, Jeremy님의 블로그
를 참고하도록 하자.)

reified 키워드를 사용하여 inline 함수를 만들면, type:Class<T>를 넘겨주지 않고도 T의 타입에 접근이 가능하다. 코드를 재작성해보자.

private inline fun <reified T> JSONObject.tryGet(key: String): T? = try {
    when (T::class) {
        Int::class -> getInt(key) as T
        Boolean::class -> getBoolean(key) as T
        String::class -> getString(key) as T
        JSONArray::class -> getJSONArray(key) as T
        Double::class -> getDouble(key) as T
      else -> null
    }
  } catch (e: Exception) {
    null
  }

이제 함수 내부에서 T의 타입에 접근할 수 있기 때문에, T로 타입캐스팅 할 때도 경고를 띄워주지 않으며 코드도 좀 더 간결해졌다.

결론

  • 제네릭스와 리플렉션을 사용하면, 타입에 따른 분기처리를 함수 내에서 가능하다. 타입 상한을 지정해서 간단하게 처리할 수도 있지만, JSONObject의 get 메서드들을 하나로 묶고 싶을 때는 이 방법이 간단하다.
  • reified 키워드를 사용하면 명시적으로 제네릭스의 타입을 넘겨주지 않아도, 함수 내부에서 제네릭스의 타입에 접근이 가능하다.
반응형