이동식 저장소

[Kotlin] 런타임에서의 제네릭과 reified 본문

Primary/Kotlin

[Kotlin] 런타임에서의 제네릭과 reified

해스끼 2022. 6. 12. 21:27

제네릭 타입은 JVM에서 실행될 때 지워진다. 런타임에 제네릭 클래스의 타입을 알 수 없다는 말이다. 이번 글에서는 Kotlin 런타임에서 타입이 실제로 어떻게 지워지는지 살펴보고, ``inline`` 함수를 선언하여 타입을 보존할 수 있는 방법을 알아본다.


런타임에서 제네릭의 동작

Java와 마찬가지로 Kotlin의 제네릭은 런타임에 지워진다. 제네릭 클래스가 어떤 타입을 담는지 런타임에 알 수 없다는 뜻이다. 예를 들어 ``List<String>`` 변수를 선언해도 런타임에서는 ``List``로 취급된다. 리스트에 담긴 개별 요소의 타입을 검사할 수는 있지만, 리스트에 ``String``만 있다고 해서 그 리스트가 ``List<String>``이라는 보장은 없다. 물론 일반적으로는 그렇게 추정할 수 있지만, 확신할 수는 없다. ``List<Any>``일 수도 있잖아?

 

런타임에 제네릭 타입이 지워지기 때문에 일어나는 일을 생각해 보자. 예를 들어 우리는 제네릭 타입을 검사할 수 없다. 이 변수가 ``String``을 담는 리스트인지, ``Number``를 담는 리스트인지 알 수 없다는 뜻이다. 아예 컴파일조차 되지 않는다.

으악

대신 어떤 변수가 ``List``인지 ``Set``인지는 검사할 수 있다. 꺾쇠 안에 ``*``을 넣으면 된다. ``*``이 무슨 역할을 하는지는 나중에 자세히 설명하겠다. 일단 지금은 임의의 타입을 나타낸다고만 알아두자.

형광색은 무시하고

제네릭 클래스도 ``as``와 ``as?``를 이용하여 형변환할 수 있다. ``List``를 ``Set``으로 형변환하면 당연히 exception이 발생한다. 그런데 ``List<Int>``를 ``List<String>``으로 형변환하면 에러가 발생하지 않는다. 애초에 타입을 모르기 때문이다. 대신 나중에 리스트의 요소인 ``Int``를 ``String``으로 간주하여 사용할 때 exception이 발생한다.

이 코드는 정상적으로 컴파일된다. ``values``에 ``List<Int>``를 전달하면 정상적으로 동작하지만, ``Set<Int>``를 전달하면 ``IllegalArgumentException``이 발생하고, ``List<String>``을 전달하면 sum을 계산하는 과정에서 ``ClassCastException``이 발생한다. ``String``을 ``Int``로 사용하려 했기 때문이다.

 

물론 컬렉션에 담긴 타입이 명시적으로 주어졌다면 형변환할 수 있다. ``Set``인지 ``List``인지는 검사할 수 있기 때문이다.

이제 제네릭 타입을 보존할 수 있는 방법에 대해 알아보자.

Reified

제네릭 타입이 지워지는 것처럼 제네릭 함수의 타입 매개변수도 지워진다. 제네릭 함수 안에서 타입 매개변수가 무엇인지 알 수 없다는 뜻이다.

T를 사용할 수 없다

하지만 inline 함수에서는 타입 매개변수를 사용할 수 있다. 컴파일 과정에서 inline 함수를 호출한 코드가 실제 함수의 본문으로 대체되므로(실제로 바이트코드를 복사한다), 타입 매개변수도 실제 타입으로 복사된다. Kotlin에서는 이것을 reified되었다고 한다.

 

보통 inline 함수는 함수 파라미터(람다)와 관련된 성능 문제를 해결하기 위해 쓰이지만, 여기서는 타입 매개변수를 사용하기 위해 함수를 inline으로 지정하였다. 

reified T

예를 들어 Collections에서 특정 타입의 원소만을 뽑아내는 ``filterIsInstance`` 라이브러리 함수는 inline과 reified 키워드를 사용하여 함수 내부에서 타입 매개변수를 참조한다.

실제 구현

활용: 클래스 참조를 reified로 대체

앱을 만들다 보면, 특정 타입의 class 변수를 참조하는 코드를 자주 볼 수 있다. 예를 들어 Activity를 시작하는 코드라던가.

val intent = Intent(context, AnotherActivity::class.java)
startActivity(intent)

inline 함수를 사용하면 class 참조를 내부로 숨기고, 겉으로 드러나는 호출부는 짧게 작성할 수 있다.

startActivity<AnotherActivity>(context)

inline fun <reified T> startActivity(context: Context) {
    val intent = Intent(context, T::class.java)
    startActivity(intent)
}

만능은 아니다

reified 키워드로 다음의 코드를 작성할 수 있다.

  • 타입 체크와 형변환 (``is``, ``as?`` 등)
  • ``KClass`` 참조 (``Type::class``)
  • Java 클래스 참조 (``Type::class.java``)
  • 다른 함수를 호출할 때 사용

하지만 다음은 할 수 없다.

  • 생성자 호출
  • ``companion object`` 호출
  • 다른 함수를 호출할 때, Reified되지 않은 타입 매개변수를 reified된 것처럼 사용
  • Mark type paremeters of classes, properties, or non-inline functions as ``reified``

 

'Primary > Kotlin' 카테고리의 다른 글

[Kotlin] 함수 타입 상속받기  (0) 2022.06.16
[Kotlin] 제네릭과 타입 간의 관계  (0) 2022.06.13
[Kotlin] 제네릭 타입 제한하기  (0) 2022.06.10
[Kotlin] &&와 and의 차이  (0) 2022.06.03
[Kotlin] for? forEach?  (0) 2022.05.25
Comments