Kotlin Generics — 실체화한 타입 파라미터
코틀린 제네릭 특징 중 실체화한 타입 파라미터에 대해 알아봅니다.
본 글은 Kotlin in Action(드미트리 제메로프, 스베트라나 이사코바 저)을 참조하여 작성하였습니다.
🔨 실행 시점의 제네릭
자바 개발자라면 알고 계시겠지만 JVM의 제네릭은 보통 타입 소거를 사용해 구현됩니다. 즉 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻입니다. 코틀린 타입 소거가 어떤 장점을 가지는지 살펴보고 함수를 inline
으로 선언함으로써 이런 제약을 어떻게 피할 수 있는지 알아보겠습니다. 함수를 inline
으로 만들면 타입 인자가 지워지지 않게 할 수 있습니다 (실체화)
자바와 마찬가지로 코틀린 제네릭 타입 인자 정보는 런타임에 지워집니다. 예를 들어 List<String>
객체를 만들고 그 안에 문자열을 여러 개 넣더라도 실행 시점에는 그 객체를 오직 List
로만 볼 수 있습니다. 이런 부분 때문에 is
검사에서 타입 인자로 지정한 타입을 검사할 수 없게 됩니다. 실행시점에 String
인지 알 수 없기 때문입니다.
if (value is Lust<String>) {...}
// ERROR : Cannot check for instance of erased type
다만 저장해야 하는 타입 정보의 크기가 줄어들어서 메모리 사용량이 줄어든다는 나름의 장점이 존재합니다. 그렇다면 어떤 값이 집합이나 맵이 아니라 리스트라는 사실을 어떻게 확인할 수 있을까요?
이 때 스타 프로젝션을 사용하면 됩니다.
if (value is List<*>) { ... }
타입 파라미터가 2개 이상이라면 모든 타입 파라미터에 *
을 포함시켜야 합니다. 일단 스타 프로젝션(*
)은 인자를 알 수 없는 제네릭 타입을 표현할 때 사용한다는 의미로 알고 있으면 될 것 같습니다. 하지만 타입 인자가 다른 타입으로 캐스팅해도 여전히 캐스팅에 성공하게 된다는 문제가 남아있습니다.
하지만 inline
함수 안에서는 타입 인자를 사용하여 함수의 본문에서 그 함수의 타입 인자를 가르킬 수 있어서 이 문제를 해결할 수 있습니다.
⚙️ 실체화한 타입 파라미터를 사용한 함수 선언
제네릭 함수가 호출되었을 때 그 함수의 본문에서 호출 시 쓰인 타입 인자를 알기 위해 인라인 함수를 사용할 수 있습니다. 어떤 함수에 inline
키워드를 붙이면 컴파일러는 그 함수를 호출한 식을 모두 함수 본문으로 바꾸게 됩니다.
그렇다면 실체화한 타입 인자는 왜 인라인 함수에서만 사용할 수 있는 걸까요? 컴파일러는 인라인 함수의 본문을 구현한 바이트코드를 그 함수가 호출되는 모든 지점에 삽입합니다. 컴파일러는 실체화한 타입 인자를 사용하여 인라인 함수를 호출하는 가 부분의 정확한 타입 인자를 알 수 있게 됩니다. 따라서 컴파일러는 타입 인자로 쓰인 구체적인 클래스를 참조하는 바이트코드를 생성해 삽입할 수 있습니다. 이 때문에 타입 파라미터가 아니라 구체적인 타입을 사용하므로 만들어진 바이트코드는 실행 시점에 벌어지는 타입 소거의 영향을 받지 않게 됩니다.
안드로이드 스튜디오의 startActivity
함수도 다음처럼 간단하게 구현할 수 있게 됩니다.
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}// usage
startActivity<DetailActivity>()
✅ 다음과 같은 경우엔 실체화한 타입 파라미터를 사용할 수 있습니다.
- 타입 검사와 캐스팅 (
is
,!is
,as
,as?
) - 코틀린 리플렉션 API(
::class
) - 코틀린 타입에 대응하는 java.lang.Class를 얻기(
::class.java
) - 다른 함수를 호출할 때 타입 인자로 사용
❗️ 다음과 같은 작업은 할 수 없습니다.
- 타입 파라미터 클래스의 인스턴스 생성하기
- 타입 파라미터 클래스의 동반 객체 메소드 호출하기
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를
reified
로 지정하기
제한되는 작업 중 마지막 제약으로 인하여 실체화한 타입 파라미터를 인라인 함수에만 사용할 수 있으므로 실체화한 타입 파라미터를 사용하는 함수는 자신에게 전달되는 모든 람다와 함께 인라이닝 됩니다. 람다 내부에서 타입 파라미터를 사용하는 방식에 따라서 람다를 인라이닝 할 수 없는 경우가 생길 수도 있고 우리가 성능 문제로 람다를 인라이닝하고 싶지 않을 수도 있습니다. 이런 경우 noinline
변경자를 함수 타입 파라미터에 붙여서 인라이닝을 금지할 수 있습니다.