Kotlin - Reflection (3) - Type Erasure on the JVM

WindSekirun (wind.seo)·2022년 4월 26일
1

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2017-10-19

Type Erasure 는 프로그램이 런타임에서 실행되기 전에 명시된 유형의 주석을 제거하는 프로세스를 뜻한다.

JVM에 익숙하다면 Generic의 Type Erasure 에 대해 알겠지만, 적어도 나는 익숙하지 않다.

문제점?

두 개의 리스트가 있다고 해보자.

val listOfStrings = listOf("One", "Two", "Three")
val listOfNumbers = listOf(1, 2, 3)

그리고 메서드를 만들어 이 리스트 안에 있는 구성요소 들이 어떤 타입인지에 따라서 다르게 처리하고 싶다.

fun <T> printList(list: List<T>) {
    when (list) {
        is List<String> -> println("This is a list of Strings")
        is List<Integer> -> println("This is a list of Integers")
    }
}

컴파일을 해보면, Cannot check for instance of erased type: List<String> 라는 오류가 발생한다.

즉, List 에서 T에 들어오는 Generic는 런타임에서 실행되기 전 지워지기 때문에 판단할 수 없다는 뜻이다.

그 이유로는 예전 JVM에서 메모리를 절약하기 위해 그렇다고 한다.

여기서 리스트 가지고 할 수 있는 것은, 겨우 이 것 밖에 없다.

if (list is List<*>) {
    println("This is a list");
}

만일 제너릭을 활용한다면 이정도 까지만 가능하다.

fun <T> printList(obj: T) {
    when (obj) {
        is Int -> println("This is an int")
        is String -> println("This is an Strings")
    }
}

이 정도 까지만 가능하지만, 이 것이 과연 우리가 원하는 것일까? 하면 아니다.

해결 방법

코틀린에서는 기존 자바와 다르게 이 문제를 해결할 수 있는 방법이 있다.

바로 inlinereified의 조합이다.

fun <T> ereased(input: List<Any>) {
    if (input is T) {
        
    }
}

이와 같은 코드가 있다고 하면, 역시 T 부분에 Cannot check for instance of erased type: T란 오류가 뜰 것이다.

T 앞에 reified 를 붙여주면, T 부분에는 오류가 없어졌지만 Only type parameters of inline functions can be reified란 오류가 뜬다.

그래서 저 함수 자체를 inline 선언을 하면 드디어 코드를 쓸 수 있게 된다.

inline fun <reified T> ereased(input: List<Any>) {
    if (input is T) {

    }
}

이 것 말고도, 실제로 들어오는 코드의 실제 타입을 알 수도 있다.

inline fun <reified T> typeInfo() {
    println(T::class)
}

부를 때는 typeInfo<String>()면 되는데, 호출 결과로는 class kotlin.String 로 String 의 실제 타입이 나온다.

구체적인 reified 에 대한 설명

https://github.com/JetBrains/kotlin/blob/master/spec-docs/reified-type-parameters.md 문서를 번역했다.

정의: '런타임-사용 가능 타입' 는 다음과 같은 경우에만 허용된다.

  • 형식 C를 가진다. 여기서 C는 타입 매개변수가 없거나 모든 타입 매개변수가 구체화 되어 있는 Classifier인데 Nothing 클래스는 제외한다.
  • G<A1,...,An> 라는 형식이 있을 때 (G는 n이라는 매개변수를 가지는 Classifier) 모든 유형 매개변수 T1는 적어도 아래의 조건 중 하나를 만족해야 한다.
    • T1는 reified 된 타입 매개변수이고, Ai는 타입 인수 이다.
    • Ai는 Star-projection 가 적용되어 있어야 한다. (List<*> 이면 A1은 Star-projection 이다.)
  • reified 된 T라는 타입 매개변수를 가지고 있어야 한다.

예제

  • 런타임-사용 가능 타입: String, Array, List<*>
  • 런타임-사용 불가능 타입: Nothing, List<String, List (for any T)
  • T가 reified 된 경우(조건부) T는 런타임-사용 가능 타입이 된다. 즉, Array 도 된다.

런타임-사용 가능 타입이 허용될 때

  • 오른쪽에 오는 요소가 is, !is, as, as? 일 때
  • reified된 타입의 인수 타입에 대한 reified 매개 변수
  • Array<List> 는 유효하다.

결과적으로 T가 reified 된 타입 매개변수이면 다음과 같은 구성이 허용됨.

  • x is T, x !is T
  • x as T, x as? T
  • T에 대한 Reflection적 접근, javaClass<T>(), T::class

reified된 매개 변수의 제한

  • inline된 함수만 가능함
  • 내장 클래스인 Array는 타입의 매개변수가 reified된 유일한 클래스. 다른 클래스는 reified된 유형 매개변수를 선언 할 수 없음
  • 런타임-사용 가능 타입만 reified된 타입 매개변수에 인수로 전달할 수 있다.

메모 inline 가능한 매개 변수를 선언하지 않고 선언된 reified 매개 변수를 갖는 inline 함수에 대해서는 Warning가 표시되지 않는다.

JVM를 위한 구현 메모

inline 함수에서 reified된 타입 매개변수 T의 발생은 실제 타입 인수로 대체된다. 실제 타입 인수가 primitive type 일 경우에는 wrapper가 reified된 바이트 코드 안에서 실행된다.

open class TypeLiteral<T> {
    val type: Type
        get() = (javaClass.getGenericSuperclass() as ParameterizedType).getActualTypeArguments()[0]
}

inline fun <reified T> typeLiteral(): TypeLiteral<T> = object : TypeLiteral<T>() {} // T는 실제 타입으로 대체된다.

typeLiteral<String>().type // returns 'class java.lang.String'
typeLiteral<Int>().type // returns 'class java.lang.Integer'
typeLiteral<Array<String>>().type // returns '[Ljava.lang.String;'
typeLiteral<List<*>>().type // returns 'java.util.List<?>'
profile
Android Developer @kakaobank

0개의 댓글