Kotlin Generics — 실체화한 타입 파라미터

hongbeom
hongbeomi dev
Published in
4 min readJun 16, 2020

코틀린 제네릭 특징 중 실체화한 타입 파라미터에 대해 알아봅니다.

본 글은 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 변경자를 함수 타입 파라미터에 붙여서 인라이닝을 금지할 수 있습니다.

읽어주셔서 감사합니다! 🙌

--

--