reified를 inline 함수에서만 사용하는 이유를 아시나요? (with. JVM 타입 소거)

송규빈·2024년 3월 6일
1
post-thumbnail

개요

🤔 reified는 왜 inline 함수에서만 사용할 수 있을까?

미디엄 블로그들의 글을 보다가 Top 10 Kotlin Questions 2024 이라는 제목에 이끌려 보게 되었다.

내용은 2024년 코틀린 질문순위 Top10개에 대한 내용인데, reified 키워드에 대해서 나와있었다.

그래서 살펴보던 중 별 생각없이 사용하던 inline 함수에서 사용하던 reified에 대한 궁금증이 생겨서 살펴보게 되었다.

이 궁금증을 풀기 전 reified 키워드에 대해서 알아야 할 필요가 있고, 자세하게 뜯어볼 예정이다.

결론부터 말하면 reified 키워드는 inline 함수에서만 사용되는 이유는, inline 함수의 인라이닝 과정을 통해서만 런타임에 제네릭 타입의 실체화가 가능하기 때문이다.

reified란?

코틀린에서 reified키워드는 제네릭 타입 파라미터에 사용된다.

inline fun <reified T> myGenericFunction() {
    val tClass = T::class
    println(tClass)
}

reified는 런타임에도 제네릭 타입 정보를 유지하고 액세스 할 수 있다.
즉, 제네릭의 타입 소거에 의한 제약들을 극복할 . 수있다.

아마도 reified 를 키워드로 검색하면 대다수의 내용이 위와 같은 내용일 것이다.

당연히 맞는 내용이고, reified를 사용하는 이유도 런타임에 제네릭 타입의 정보를 얻을 수 있기 때문이기도 하다.

자바나 코틀린의 일반 제네릭에서는 런타임에 제네릭 타입 정보가 소거(erase)된다.
이러한 이유는 타입 소거(type erasure)라고 불리는 JVM의 특성 때문이다.

타입 소거는 무엇이고, 왜 이루어지는가?

타입 소거라는 말을 보면 대충 말 뜻은 알겠지만 구체적으로 설명해보라고 하면 어려울 수도 있다.

제네릭(Generic)

제네릭은 코드에서 타입(type)을 파라미터화할 수 있게 해주는 프로그래밍 기능이다.
예를 들어 List <T>에서 T는 어떤 타입이든 될 수 있다.
이는 같은 코드를 다양한 타입에 대해 재사용할 수 있게 해주며, 타입 안정성을 향상시키고, 캐스팅 오류를 줄여준다.

val strings : List<String> = ArrayList()
val integers : List<Int> = ArrayList()

타입 소거(Type Erasure)란?

타입 소거는 컴파일 시점에 제네릭 타입의 정보를 검사하고 확인한 후, 런타임에는 이 타입 정보를 제거하는 과정이다.
즉, 런타임에는 제네릭 타입 파라미터가 특정한 구체적인 타입으로 대체되거나, 가장 가까운 제한 타입(bound)으로 대체된다.

생겨난 이유
타입 소거는 제네릭이 도입된 주된 이유 중 하나인 JVM(Java Virtual Machine)의 호환성을 위한 메커니즘이다.

제네릭은 자바 5에서 도입이 되었다. 그렇기 때문에 이전 버전의 자바와도 호환이 가능하게끔 타입 소거가 필요한 것이다.

타입 소거가 되는 과정
타입 소거는 제네릭 타입의 경계에 따라서 달라지는데, 이 경계에 대해서는 찾아보길 바란다.
이 글에서는 경계가 없는 경우를 다룰 것이다.
타입 매개변수의 경계가 없는 경우에 타입 소거를 하게 되면 Object로 치환이 된다.

추가적으로 타입 안정성을 위해 필요한 경우에는 타입 변환을 추가하고, 제네릭 타입을 상속받은 클래스의 다형성을 유지하기 위해 Bridge method라는 것을 생성하기도 한다.

타입 소거가 되면?

런타임 타입 정보의 부재

런타임에 제네릭 타입 정보가 소거되기 때문에 이 정보를 런타임에 직접적으로 조회하거나 사용할 수 없다.

메서드 오버로딩의 제한

컴파일 후에 같은 메서드 시그니처로 간주되기 때문에 메서드 오버로딩이 제한됩니다.

편의를 위해 Java 코드로 작성했지만, 코틀린 코드로도 보자.

fun main() {
    print(listOf("1"))

    print(listOf(1))
}

fun print(c: List<String>) {
    println(c)
}

fun print(c: List<Int>) {
    println(c)
}

위 코드를 IDE에서 작성해보면 컴파일 타임에는 아무런 에러가 나오지 않는다. 하지만 실행을 해보면 아래와 같은 중복 에러를 마주할 것이다.

reified는 어떻게 타입 소거에 대한 제약을 극복했나?

내 궁금증에 대한 해답

이 파트를 보면 reified는 왜 inline 함수에서만 사용할 수 있을까?에 대한 내 궁금에 대한 해답을 알 수 있다.

위에서도 설명했지만 reified 키워드를 사용하면 런타임에 제네릭 타입의 정보를 얻을 수 있다.

Inline 함수

코틀린에서 inline함수는 컴파일 시에 함수 호출이 함수 본문으로 대체되는 특별한 타입의 함수이다. 즉, 함수가 호출된 위치에 함수의 내용이 그대로 삽입된다.
성능 향상을 위해 사용되며, 함수 호출의 오버헤드를 줄일 수 있다.

non-inline 함수

inline 함수

reified와의 동작 원리

reified 키워드를 사용하면 타입 파라미터가 실제화되며 런타임에도 그 타입 정보가 유지된다고 했다.
하지만 reified 키워드는 inline 함수와 함께 사용해야 되는데, 동작 원리와 함께 이유를 알아보자.

inline fun <reified T> myGenericFunction() {
    val tClass = T::class
    println(tClass)
}
  1. inline함수가 호출되면, 그 함수의 코드가 호출 지점에 직접 삽입된다.
  2. reified 키워드가 적용된 제네릭 타입은 함수가 인라인되면서 타입 파라미터가 구체적인 타입으로 대체된다. 이로 인해 런타임에도 해당 타입의 정보를 유지하고 접근할 수 있게 된다.

fun <T> isT(value: Any) = value is T

reified 키워드가 사용되지 않은 제네릭 함수일 때는 타입 소거가 발생되기 때문에 위와 같은 에러가 나온다.

inline fun <reified T> isT(value: Any) = value is T

해당 코드를 작성해서 실행해보면 inline함수 내의 제네릭 타입 파라미터가 실제 사용된 구체적인 타입으로 치환되기 때문에 정상 작동하는 것을 볼 수 있다.

reified는 왜 inline 함수에서만 사용할 수 있을까?

💡 inline 함수의 인라이닝 과정을 통해서만 런타임에 제네릭 타입의 실체화가 가능하기 때문

위와 같은 동작 원리로 인해 reified 키워드는 inline 함수에서만 의미를 가지며, inline함수에서만 사용할 수 있다.

결론에 대해 조금 더 자세히 말하면 inline 함수에서 코드가 호출 지점에 직접 삽입되기 때문에 이 과정에서 제네릭 타입 파라미터는 그 호출 지점의 구체적인 타입으로 대체될 수 있다.

profile
🚀 상상을 좋아하는 개발자

0개의 댓글