728x90
반응형
✅ 실행 시 제네릭스의 동작 : 소거된 타입 파라미터와 실체화된 타입 파라미터
- JVM의 제네릭스는 보통 타입 소거를 사용해 구현된다.
- 이는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻이다.
타입 소거(type erasure)란?
- 원소 타입을 컴파일 타임에만 검사하여 런타임에는 해당 타입 정보를 알 수 없게 하는 것
- 컴파일 타임에만 타입에 대한 제약 조건이 적용되며, 런타임에는 타입에 대한 정보를 제거한다는 뜻이다.
- 이번 절에서는 코틀린 타입 소거가 실용적인 면에서 어떤 영향을 끼치는지 살펴보고 함수를 inline으로 선언함으로써 이런 제약을 어떻게 우회할 수 있는지 살펴본다.
- 함수를 inline으로 만들면 타입 인자가 지워지지 않게 할 수 있다.
- 코틀린에서는 이를 실체화라고 부른다.
- 실체화한 타입 파라미터에 대해 자세히 다루며, 유용한 이유에 대해 살펴보자.
📌 실행 시점의 제네릭 : 타입 검사와 캐스트
- 자바와 마찬가지로 코틀린 제네릭 타입 인자 정보는 런타임에 지워진다.
- 이는 제네릭 클래스 인스턴스가 그 인스턴스를 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않는다는 뜻이다.
- 예를 들어 List<String> 객체를 만들고 그 안에 문자열을 여럿 넣더라도 실행 시점에는 그 객체를 오직 List로만 볼 수 있다.
- 그 List 객체가 어떤 타입의 원소를 저장하는지 실행 시점에는 알 수 없다.
- 다음 두 리스트에 대해 코드를 실행할 경우 어떤 일이 벌어지는지 생각해보자.
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)
- 컴파일러는 두 리스트를 서로 다른 타입으로 인식하지만 실행 시점에 그 둘은 완전히 같은 타입의 객체다.
- 그럼에도 불구하고 보통은 list1에는 문자열만, list2에는 정수만 들어있다 가정할 수 있는데, 이는 컴파일러가 타입 인자를 올바른 타입의 값만 각 리스트에 넣도록 보장해주기 때문이다.
- 이러한 타입 소거로 인해 생기는 한계는 과연 무엇이 있을까?
- 타입 인자를 따로 저장하지 않기 때문에 실행 시점에 타입 인자를 검사할 수 없다.
- 예를 들면 어떤 리스트가 문자열로 이뤄진 리스트인지 다른 객체로 이뤄진 리스트인지를 실행 시점에 검사할 수 없다.
- 일반적으로 말하자면 is 검사에서 타입 인자로 지정한 타입을 검사할 수는 없다.
if (value is List<String>) { ... }
// ERROR : Cannot check for instance of erased type
- 실행 시점에 어떤 값이 List인지 여부는 알아낼 수 있지만 그 리스트가 String의 리스티인지, Int의 리스트인지, 다른 타입의 리스트인지는 알 수가 없다.
- 이러한 타입 정보는 타입 소거로 지워지기 때문이다.
- 하지만 저장해야 하는 타입 정보의 크기가 줄어들어 전반적인 메모리 사용량이 줄어든다는 제네릭 타입 소거 나름의 장점이 있다.
- 앞서 말한 대로 코틀린에서는 타입 인자를 명시하지 않고 제네릭 타입을 사용할 수는 없다.
- 그렇다면 어떤 값이 집합이나 맵이 아닌 리스트라는 사실을 확인할 수 있는 방법은 없을까?
- 스타 프로젝션(star projection)을 사용하면 확인이 가능하다.
if (value is List<*>) { ... }
- 지금은 알 수 없는 제네릭 타입을 표현할 때 (자바의 List<?>와 유사) 스타 프로젝션을 사용한다.
- 하지만 이는 value가 List임을 알 수는 있지만 그 원소 타입은 알 수 없다.
- as나 as? 캐스팅에도 여전히 제네릭 타입을 사용할 수 있다.
/* 제네릭 타입으로 타입 캐스팅하기 */
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> // 여기서 Unchecked cast: List<*> to List<Int> 경고 발생
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
printSum(listOf(1, 2, 3)) // 예상대로 작동
// 6
- 하지만 이때 타입 인자가 다른 타입으로 캐스팅할 경우에도 캐스팅에 성공한다는 점을 조심해야 한다.
- 컴파일러가 "unchecked cast"라는 경고를 해주지만, 단순 경고만 하고 컴파일을 진행하기 때문에 모든 코드가 문제 없이 컴파일된다.
- 추가 예제를 살펴보자.
printSum(setOf(2, 3, 4)) // 집합은 리스트가 아니기 때문에 예외가 발생한다.
// IllegalArgumentException: List is expected
printSum(listOf("a", "b", "c")) // as? 캐스팅은 성공하지만 후에 다른 예외가 발생한다.
// ClassCastException: String cannot be cast to Number
- 위 예제에서 첫 번째의 경우 리스트가 아닌 집합에 대해서는 IllegalArgumentException이 발생하는 것을 볼 수 있다.
- 하지만 두 번째의 경우 다른 예외를 발생시킨다.
- as? 캐스트가 성공하고 문자열 리스트에 대한 sum 함수가 호출되기 때문이다.
- 이때, sum이 실행되는 도중에 예외가 발생하고 String을 Number로 사용할 수 없기 때문에 ClassCastException이 발생하는 것을 볼 수 있다.
- 코틀린 컴파일러는 컴파일 시점에 타입 정보가 주어졌을 경우 is 검사를 수행할 수 있도록 허용한다.
/* 알려진 타입 인자를 사용해 타입 검사하기 */
fun printSum(c: Collection<Int>) {
if (c is List<Int>) { // 이 검사는 올바르다.
println(c.sum())
}
}
printSum(listOf(1, 2, 3))
// 6
- 컴파일 시점에 Collection이 Int 값을 저장한다는 사실이 알려졌기 때문에 c가 List<Int>인지 검사할 수 있다.
- 이처럼 코틀린은 제네릭 함수에서 타입 인자를 가리킬 수 있는 특별한 기능을 제공하지 않는다.
- 하지만 inline 함수 안에서는 타입 인자를 사용할 수 있다.
📌 실체화한 타입 파라미터를 사용한 함수 선언
- 앞서 설명했던 것처럼 코틀린 제네릭 타입의 타입 인자 정보는 런타임 시점에 지워진다.
- 제네릭 함수의 타입 인자도 마찬가지이다.
fun <T> isA(value: Any) = value is T
// Error: Cannot check for instance of erased type: T
- 하지만 이러한 제약은 inline 함수를 활용하면 피할 수 있다.
- 이라인 함수의 타입 파라미터는 실체화되어 실행 시점에 인라인 함수의 타입 인자를 알 수 있기 때문이다.
- 인라인 함수에 대해서는 아래 글을 통해 확인할 수 있다.
[Kotlin In Action] 8장. 고차 함수 : 파라미터와 반환 값으로 람다 사용(2) - 인라인 함수 : 람다의 부가
✅ 인라인 함수 : 람다의 부가 비용 없애기 코틀린에서 람다를 함수 인자로 넘기는 구문은 if나 for와 같은 일반 문장과 비슷하다. 5장에서 살펴본 with와 apply 함수가 그런 예다. [Kotlin In Action] 5장.
dahoonkk.tistory.com
- 인라인 함수는 컴파일러가 그 함수를 호출한 식을 모두 함수 본문으로 바꾼다.
- 또한 함수가 람다를 파라미터로 사용하는 경우 그 함수를 인라인 함수로 생성 시 람다 코드도 함께 인라이닝되어 무명 클래스와 객체가 생성되지 않아 성능이 더 좋아질 수 있다.
- 그럼 이제 인라인 함수가 유용한 또 다른 이유인 타입 인자 실체화에 대해 알아보자.
- 위 예제의 isA 함수를 인라인 함수로 바꾸고 타입 파라미터를 reified로 지정하면 value의 타입이 T의 인스턴스인지를 실행 시점에 검사할 수 있다.
/* 실체화한 타입 파라미터를 사용하는 함수 정의하기 */
inline fun <reified T> isA(value: Any) = value is T // 이제는 이 코드가 컴파일된다.
println(isA<String>("abc'))
// true
println(isA<String>(123))
// false
- 이렇게 실체화된 타입 파라미터를 화라용하는 가장 간단한 예제 중 하나는 표준 라이브러리 함수인 filterIsInstance이다.
- 이 함수는 인자로 받은 컬렉션의 원소 중 타입 인자로 지정한 클래스의 인스턴스만을 모은 후 리스트로 반환해준다.
/* filterIsInstance 표준 라이브러리 함수 사용하기 */
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
// [one, three]
- filterIsInstance의 타입 인자로 String을 지정하여 문자열만 가져올 수 있도록 기술한다.
- 따라서 이 함수의 반환 타입은 List<String>이 된다.
- 여기서 우리가 알아야 할 것은 타입 인자를 실행 시점에 알 수 있고, filterIsInstance는 해당 타입 인자를 활용해 해당 타입과 일치하는 원소만을 추려낼 수 있다.
❗ 인라인 함수에서만 실체화한 타입 인자를 쓸 수 있는 이유는 뭘까?
- 컴파일러는 인라인 함수의 본문을 구현한 바이트코드를 해당 함수가 호출되는 모든 시점에 삽입한다.
- 또한, 컴파일러는 타입 인자가 실체화되었기 때문에 인라인 함수를 호출하는 각 부분에 대해 정확한 타입 인자를 알 수 있다.
- 따라서 컴파일러는 타입 인자로 쓰인 구체적인 클래스를 참조하는 바이트코드를 생성해 삽입할 수 있게 된다.
- 주의할 점은 자바에서는 reifieㅇ 타입 파라미터를 사용하는 inline 함수를 호출할 수 없다.
- 호출할 경우 다른 보통 함수처럼 호출하기 때문에 아무리 인라인 함수를 호출해도 실제로 인라이닝 되지 않는다.
📌 실체화한 타입 파라미터로 클래스 참조 대신
- java.lang.Class 타입 인자를 파라미터로 받는 API에 대한 코틀린 어댑터를 구축할 경우 실체화한 타입 파라미터를 자주 사용한다.
- 간단한 예로 안드로이드의 startActivity를 들 수 있다.
- 액티비티의 클래스를 java.lang.Class로 전달하는 대신 실체화한 타입 파라미터를 사용할 수 있다.
inline fun <reified T: Activity>
Context.startActivity() { // 타입 파라미터를 "reified"로 표시
val intent = Intent(this, T::class.java) // T::class로 타입 파라미터의 클래스를 가져옴
startActivity(intent)
}
startActivity<DeatailActivity>() // 액티비티를 표시하기 위해 메소드 호출
- 여기서 ::class.java 구문은 코틀린 클래스에 대응하는 java.lang.Class 참조를 얻는 방법을 보여준다.
📌 실체화한 타입 파라미터의 제약
- 실체화한 타입 파라미터는 유용한 도구지만 몇 가지 제약이 있다.
- 다음은 실체화한 타입 파라미터의 사용 가능한 경우와 사용 불가능한 경우를 보여준다.
- 사용 가능
- 타입 검사와 캐스팅(is, !is, as, as?)
- 코틀린 리플렉션 API(::class)
- 코틀린 타입에 대응하는 java.lang.Class 얻기(::class.java)
- 다른 함수 호출 시 타입 인자로 활용
- 사용 불가능
- 타입 파라미터 클래스의 인스턴스 생성
- 타임 파라미터 클래스의 동반 객체 메소드 호출
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정
- 여기서 사용 불가능 내역 중 마지막 제약으로 인한 한 가지 흥미로운 효과가 있다.
- 실체화한 타입 파라미터는 인라인 함수에서만 사용 가능하기 때문에 실체화한 타입 파라미터를 사용하는 함수는 자신에게 전달되는 모든 람다와 함께 인라이닝된다.
- 하지만 이때 람다 내부에서 타입 파라미터를 사용하는 방식에 따라서는 람다를 인라이닝할 수 없는 경우가 생기기도 하고 성능 문제로 인해 람다를 인라이닝 하고 싶지 않을 수도 있다.
- 이러한 경우 noinline을 함수 타입 파라미터에 붙여 사용하면 인라이닝을 금지할 수 있다.
- 사용 가능
728x90
반응형