제네릭 타입의 파라미터를 받은 뒤
해당 타입의 클래스를 얻는 과정에서
Cannot use 'R' as reified type parameter. Use a class instead. 컴파일 에러가 났다.
이 에러를 해결하기 위해 Type erasure의 개념과 inline functions에서의 reified type parameters 사용법에 대해 알아보자.
얻어갈 지식
- Type ensure 기능
- inline functions의 reified type parameters 사용 법
"에러의 상황"
( 코드를 약간 각색하였습니다. )
JPA 검색 결과인 Page<Sms>를 pageDto<SmsDto>로 변환하는 함수가 있다.
fun getPageDtoBySmsPage(pageSms: Page<Sms>): PageDto<SmsDto> {
return PageDto(
pageSms.content.map { modelMapper.map(it, SmsDto::class.java) }.toList(),
)
}
위 함수는 오직 제네릭 타입이 Sms인 경우에만 사용할 수 있고 다른 제네릭 타입의 경우 위 함수를 사용할 수 없다.
이를테면, Page<Member>는 위 함수의 파라미터로 넘길 수 없고 함수를 통해 PageDto<MemberDto>를 받고 싶다면
아래와 같이 거의 똑같은 함수를 하나 더 만들어야 한다는 것이다.
fun getPageDtoByMemberPage(pageMember: Page<Member>): PageDto<MemberDto> {
return PageDto(
pageMember.content.map { modelMapper.map(it, MemberDto::class.java) }.toList()
)
}
하지만 Page의 제네릭 타입이 바뀔 때마다 새로 함수를 만들어야 한다는 것은 매우 비효율적이므로,
위 두 함수를 하나의 제네릭 함수로 수정하였다.
fun<T, R> getPageDtoByPage(pageResponse: Page<T>): PageDto<R> {
return PageDto(
pageResponse.content.map { modelMapper.map(it, R::class.java) }.toList()
)
}
이렇게 작성한 뒤, 실행하려는데 R::class.java 부분에 컴파일 에러가 났다.
"Cannot use 'R' as reified type parameter. Use a class instead"
'R'을 reified type parameter로 사용할 수 없습니다. 대신 클래스를 사용하세요.
R을 reified type parameter로 사용할 수 없으니 애초에 class를 파라미터로 받으라는데,
왜 R을 받았는데 R::class.java로 사용하지 못하는 것이고
왜 R을 reified type parameters로 사용하려는 것일까
"Type erasure"
코틀린 공식문서 참조.
Type erasure
The type safety checks that Kotlin performs for generic declaration usages are only done at compile time. At runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. For example, the instances of Foo<Bar> and Foo<Baz?> are erased to just Foo<*>.
Therefore, there is no general way to check whether an instance of a generic type was created with certain type arguments at runtime, and the compiler prohibits such is -checks.
Type casts to generic types with concrete type arguments, for example, foo as List<String>, cannot be checked at runtime.
These unchecked casts can be used when type safety is implied by the high-level program logic but cannot be inferred directly by the compiler. The compiler issues a warning on unchecked casts, and at runtime, only the non-generic part is checked (equivalent to foo as List<*> ).
The type arguments of a generic function calls are also only checked at compile time. Inside the function bodies, the type parameters cannot be used for type checks, and type casts to type parameters (foo as T) are unchecked. However, reified type parameters of inline functions are substituted by the actual type arguments in the inlined function body at the call sites and thus can be used for type checks and casts, with the same restrictions for instances of generic types as described above.
Type erasure
1. 제네릭 사용에 대해 Kotlin이 수행하는 type safety check는 컴파일 시간에만 수행됩니다. 런타임에 제네릭 타입의 인스턴스는 해당 실제 타입을 갖고 있지 않습니다. 이것을 type information이 지워졌다고 말합니다. 예를 들어 Foo<Bar>및 Foo<Baz?>의 인스턴스는 런타임 시 Foo<*>로 지워집니다.
2. 따라서, 제네릭 유형의 인스턴스가 런타임에 특정 형식 인수를 사용하여 생성되었는지 여부를 확일할 일반적인 방법은 없으며 컴파일러는 이러한 is -checks(타입을 확인하는 예약어)를 허용하지 않습니다.
3. 구체적인 type arguments가 있는 제네릭 유형으로의 캐스트는 런타임에 확인할 수 없습니다. (ex: foo as List<String>)
이러한 unchecked casts는 high-level program logic에 의해 암시되지만 컴파일러에서 직접 추론할 수 없는 경우 사용할 수 있습니다. 컴파일러는 unchecked casts에 대해 경고를 하고, 런타임 시 제네릭이 아닌 부분만 검사합니다.
4. 제네릭 함수의 type arguments도 컴파일 타임에만 확인됩니다. 함수 본문 안에서 type parameters는 type check에 사용될 수 없으며, type parameters로 type cast를 하면 uncheked가 됩니다. ( ex: foo as T ) 그러나, inline functions의 reified type parameters는 호출 장소에서 인라인 함수 본문의 실제 type arguments로 대체되므로 위에서 설명한 것과 같은 제네릭 타입의 인스턴스에 대한 동일한 제한 사항과 함께 type check와 type case에 사용될 수 있다.
정리하면 아래와 같다.
1. 제네릭을 써서 코드를 작성할 때 type check는 컴파일 시간에만 수행되고, 런타임 시 모든 제네릭은 지워진다.
예를 들어, 일단 아래와 같은 변수가 있다고 생각하자
val stringList = listOf("a", "b", "c")
코드를 작성하는 시점에서 컴파일러는 해당 리스트 안에 아이템을 모두 String type으로 인지하지만,
런타임 시 해당 type information이 모두 지워지기 때문에 해당 아이템들이 String type인지 무슨 타입인지 모른다는 것.
2. 런타임 시점에 제네릭 유형의 인스턴스가 어떤 특정 형식 따라서 생성되었는지 모른다.
따라서, 런타임 시 아래와 같은 is -check는 런타임 에러를 내뱉는다.
fun <T> printList(list: List<T>) {
when (list) {
is List<String> -> println("List<String>")
is List<Integer> -> println("List<Integer>")
}
}
3. 아래와 같은 unchecked cast는 런타임 시점에 타입 안전성을 확인할 수 없다.
fun <T> converter(list<T>: List<T>): List<String> {
// 컴파일러가 경고메세지를 주지만 실행은 된다.
// <T> 가 String 타입이면 문제없이 리턴하지만
// String 타입이 아닌경우 해당 캐스트를 하는 시점에 에러가 난다.
return list as List<String>
}
4. inline funtions의 reified type parameters는 함수 본문의 실체 type arguments로 대체되므로,
type check도 가능하고 type case에도 사용될 수 있다.
이를테면 아래와 같은 코드를
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress("UNCHECKED_CAST")
return p as T?
}
inline functions의 reified type parameter를 사용하여 아래와 같이 깔끔하게 쓸 수 있다.
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}
"결론"
Cannot use 'R' as refied type parameter. Use a class instead 오류를 냈던 코드를 다시보자
fun<T, R> getPageDtoByPage(pageResponse: Page<T>): PageDto<R> {
return PageDto(
pageResponse.content.map { modelMapper.map(it, R::class.java) }.toList()
)
}
위 함수에서 R:class.java부분을 컴파일러가 오류로 인식하는 이유는
Type erasure 기능으로 런타임시 R의 type information이 지워져, R::class.java를 가져올 수 없기 때문이다.
이 문제를 해결하려면 아래와 같이 inline functions의 reified type parameters를 사용하면 된다.
inline fun<T, reified R> getPageDtoByPage(pageResponse: Page<T>): PageDto<R> {
return PageDto(
pageResponse.content.map { modelMapper.map(it, R) }.toList()
)
}
이제 우리는 앞에서 물었던 질문에 대해 답할 수 있다.
왜 R을 받았는데 R::class.java로 사용하지 못하는 것이고
=> Type erasure 때문에 R의 type information이 지워졌기 때문이다.
왜 R을 reified type parameters로 사용하려는 것일까
=> R::class.java를 사용하려면 R의 type information이 지워지지 않아야 하는데 reified type parameters가 해당 조건을 만족시키기 때문에 컴파일러가 R을 reified type parameters로 사용하려 하기 때문이다.
추가 질문
- inline 함수란 무엇인가
- inline 함수에서만 reified type parameters를 쓸 수 있는 이유
- reified란
- T::class.java의 의미
- Type erasure 기능은 왜 있는 것일까
참고 문서
'코틀린' 카테고리의 다른 글
코틀린 { apply, with, let, also, run } 이해 (0) | 2021.06.30 |
---|