Kotlin - Reified를 사용하는 이유?

JS · 28 Jun 2020

Reified 키워드는 Generics로 inline function에서 사용되며, Runtime에 타입 정보를 알고 싶을 때 사용합니다.

구체적으로 언제 이 키워드가 필요할까요? Generics는 아래와 같이 T와 같은 문자로 타입을 표현합니다. 여기서 T는 어떤 타입이든 될 수 있죠.

fun <T> function(argument: T)

Generics 코드를 컴파일할 때 컴파일러는 T가 어떤 타입인지 알고 있습니다. 하지만 Compile하면서 타입 정보를 제거하여 Runtime에는 T가 어떤 타입인지 모릅니다. 그냥 T로 정해진 객체가 존재할 뿐이죠.

Reified 키워드를 사용하면 Generics function에서 Runtime에 타입 정보를 알 수 있습니다. 하지만 inline function과 함께 사용할 때만 가능합니다.

inline function은 함수 내의 코드를 inline으로 호출하는 위치에 붙여넣어주는 기능입니다. 함수를 호출하지 않기 때문에 성능이 좋습니다. 하지만 긴 코드에 적용을 한다면 코드 양이 많아지는 성능 문제가 발생할 수 있습니다. 더 자세한 것은 Kotlin - inline functions 이해하기을 참고해주세요.

Generics에서 타입 정보를 아는 방법

Reified 키워드를 사용하면 Generics function에서 Runtime에 타입 정보를 알 수 있다고 했습니다. Reified 키워드 없이 타입 정보를 알고 싶다면 어떻게 해야 할까요?

다음 코드를 한번 보세요. 이 코드는 Generics가 아닙니다. 타입이 String으로 고정되어있습니다.

fun printString(value: String) {
    when (value::class) {
        String::class -> {
            println("String : $value")
        }
    }
}

printString("print string function")

코드를 실행해보면 의도한 문자열이 출력됩니다.

String : print string function

위의 코드를 Generics로 만들고 싶습니다.

다음과 같이 타입에 따라 출력하는 내용을 다르게 만들고 싶습니다. 하지만 아래 코드는 컴파일 에러가 발생합니다. Runtime에 타입 정보가 지워지기 때문입니다.

fun <T> printGenerics(value: T) {
    when (value::class) {  // compile error!
        String::class.java -> {
            println("String : $value")
        }
        Int::class.java -> {
            println("Integer : $value")
        }
    }
}

이럴 때 타입 정보가 있는 객체를 인자로 전달하면 문제를 해결할 수 있습니다.

아래와 같이 Class<T>로 인자를 전달하였고 이것으로 타입을 알 수 있습니다.

fun <T> printGenerics(value: T, classType: Class<T>) {
    when (classType) {
        String::class.java -> {
            println("String : $value")
        }
        Int::class.java -> {
            println("Int : $value")
        }
    }
}

printGenerics("print generics function", String::class.java)
printGenerics(1000, Int::class.java)

위의 코드를 실행해보면 다음과 같이 출력됩니다.

String : print generics function
Integer : 1000

지금까지 Reified 키워드 없이 Runtime에 Generics의 타입 정보를 알 수 있는 예제를 작성해보았습니다. 물론 위의 코드를 사용한다고 해서 문제될 것은 없지만, 타입 정보를 얻기 위해 인자 1개를 추가로 넘겨야하는 것이 깔끔해보이진 않습니다.

Reified 키워드를 사용하여 타입 정보 얻기

Reified는 다음과 같이 정의할 수 있습니다. reified는 inline에서만 사용할 수 있기 때문에 inline으로 함수를 정의하였고, Generics의 타입 T 앞에 reified 키워드를 써주면 됩니다.

inline fun <reified T> function(argument: T)

위에서 구현한 예제를 reified 키워드를 사용하여 다시 구현해보겠습니다.

reified 키워드를 사용했기 때문에 T::class처럼 클래스의 타입 정보를 얻을 수 있습니다. 그래서 타입 정보를 인자로 넘기지 않아도 Runtime에 타입 정보를 알 수 있습니다.

inline fun <reified T> printGenerics(value: T) {
    when (T::class) {
        String::class -> {
            println("String : $value")
        }
        Int::class -> {
            println("Int : $value")
        }
    }
}

printGenerics1("print generics function")
printGenerics1(1000)

위의 코드를 실행해보면 다음과 같이 출력됩니다.

String : print generics function
Integer : 1000

함수 오버로딩

reified를 사용하면 리턴 타입이 다른 함수를 오버로딩 할 수 있습니다.

아래와 같이 리턴 타입은 다르고 인자와 함수 이름은 동일한 함수를 정의하고 싶습니다. 리턴 타입에 따라서 호출되는 함수가 달라졌으면 합니다. 하지만 아래 코드는 컴파일이 안됩니다. 자바는 이름과 인자가 동일하면 오버로딩이 안되기 때문입니다.

// compile error!
fun getMessage(number: Int): String {
    return "The number is : $number"
}

fun getMessage(number: Int): Int {
    return number
}

reified를 사용하면 Generics function으로 위에서 의도한 내용을 구현할 수 있습니다.

아래 코드는 타입에 따라 리턴되는 타입이 다르도록 구현되었습니다.

inline fun<reified T> getMessage(number: Int): T {
    return when (T::class) {
        String::class -> "The number is : $number" as T
        Int::class -> number as T
        else -> "Not string, Not Integer" as T
    }
}

위의 함수는 아래와 같이 사용할 수 있습니다. 리턴되는 타입에 따라서 T가 결정되며 T에 따라서 리턴되는 객체가 달라집니다.

val result : Int = getMessage(10)
println("result: $result")

val resultString : String = getMessage(100)
println("result: $resultString")

위의 코드를 실행하면 결과는 다음과 같습니다.

result: 10
result: The number is : 100

정리

reified를 사용하는 이유에 대해서 알아보고 어떻게 사용하는지 알아보았습니다.

참고

codechachaCopyright ©2019 codechacha