Backend/Kotlin

[Kotlin] 제네릭(내가 필요한 것만 정리..)

findmypiece 2021. 12. 6. 16:52
728x90

타입 파라미터와 타입 인자

제네릭 클래스나 함수를 정의할 때에는 T 같은 것들을 타입 파라미터라 하고 객체를 생성하거나 함수를 호출할 때 지정하는 실제 타입을 타입인자라고 부른다.

 

자바에서는 제네릭 타입을 선언할 때 타입 파라미터나 인자가 없는 raw 타입을 허용하는데 예를 들어 List를 선언할 때를 생각해볼 수 있다.

List list = new ArrayList();

 

하지만 코틀린에서는 이렇게 선언할 수 없으며 아래와 같이 반드시 타입 파라미터 또는 인자를 지정해야 한다. 자바는 제네릭을 1.5 버전 부터 도입했기 때문에 하위 호환성을 위해 raw 타입을 허용하지만 코틀린은 처음부터 제네릭을 지원했기 때문에 타입 인자를 반드시 정의해야 한다.

//타입 파라미터 지정
var list: List<String> = mutableListOf();

//타입 인자 지정
var list = mutableListOf<String>();

 

타입추론

물론 자바든 코틀린이든 아래와 같이 타입추론이 가능한 상황이라면 raw 타입이 허용되는 점은 동일하다.

List list = Arrays.asList("AAA", "BBB");

var list = listOf("AAA", "BBB")

 

이러한 타입추론은 함수에도 적용된다. 원래 제네릭 함수를 호출 할때는 구체적인 타입인자를 반드시 넘겨야 하지만 대부분 컴파일러가 타입 추론이 가능한 상황이기 때문에 생략이 가능하다.

 

예를 들어 컬랙션을 다루는 slice 함수를 생각해보자.

 

fun <T> LIst<T>.slice(indices: IntRange): List<T>

 

원래는 해당 함수를 함수를 호출할 때 타입인자를 반드시 넘겨야 하지만 타입 추론이 가능하기 없이도 호출이 가능하다.

val letters = ('a'..'z').toList()

//타입인자 명시적으로 지정
println(letters.slice<Char>(0..2))

//컴파일러가 타입 추론
println(letters.slice(0..2))

 

제네릭 타입은 기본적으로 null 을 허용한다

코틀린에서는 타입 지정시 기본적으로 null 을 포함하지 않고 null을 허용할 경우 타입 뒤에 ? 를 포함해야 한다. 그런데 제네릭의 경우 기본적으로 null 을 포함한다. 

 

예를 들어 아래와 같이 제네릭 함수를 정의했다면 <T> 는 기본적으로 null 을 허용하기 때문에 아래와 같이 엘비스 연산자를 통해 안전한 호출하는 것이 좋다.

fun <T> test(str: T): String = "${str?:"none"} test"

 

이렇듯 <T> 의 기본 상한선은 Any? 와 같다. 이에 제네릭 타입에 에초에 null 을 허용하고 싶지 않다면 아래와 같이 상한을 지정하면 된다.

fun <T: Any> test(str: T): String = "$str test"

 

타입소거(type erasure)

제네릭은 타입소거를 사용해서 구현된다. 코틀린 기준 제네릭을 선언하거나 호출할 때는 타입 파라미터나 인자 정의가 강제되기도 하고 컴파일러가 타입추론을 하기도 하지만 런타임 시점에는 이런 타입 인자 정보가 제거된다는 말이다.

 

타입 추론이 가능한 상황이라면 타입 정보가 없더라도 의도한데로 동작할테지만 타입 인자에 대한 정보가 없기 때문에 타입 인자로 지정한 타입을 체크할 순 없다.

 

하지만 런타임 시점에 타입 인자에 대한 정보를 알아야 하는 경우가 있다. 예를 들어 아래와 같이 제네릭 함수 안에서 또 다른 제네릭 함수를 호출하는 경우이다.

object ObjectMapperUtil {
    val mapper = jacksonObjectMapper()

    init {
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        mapper.registerModule(JavaTimeModule())
    }
    
    inline fun <reified T: Any> String.toObject(valueType: KClass<T>): T = mapper.readValue(this)
    
}

위 코드는 코틀린에서 ObjectMaper 유틸을 구현한 내용이다. 역직렬화를 위해 ObjectMapper.readValue 함수를 호출해야 하는데 별도로 커스텀된 ObjectMapper 객체를 사용하기 위해 위와 같이 ObjectMapperUtil object 에 확장함수를 정의하였다.

 

위와 같은 상황에서는 별도로 구현한 toObject 함수 안에서 또다시 제네릭 함수인 ObjectMapper.readValue 함수를 호출해야 한다. 그리고 알다시피 제네릭 함수는 호출시 구체적인 타입 정보가 필요하고 이에 toObject 함수 실행시점에 전달된 타입 인자 정보가 필요하다.

 

이에 위와 같이 inline, reified 키워드를 통해 런타임 시점에도 타입 인자에 대한 정보가 erasure 되지 않고 유지되도록 한다. 코드에 대한 설명을 조금 보태자면 아래와 같다.

  1. 런타임 시 티입 인자가 소거되지 않도록 하려면 기본적으로 inline 키워드를 통해 해당 함수를 호출한 식을 함수 본문에 포함시켜야 한다.
  2. 본문에 포함된 함수식 중에서도 reified 키워드에 의해 해당 타입 파라미터가 실행 시점에 소거되지 않도록 한다.

 

그런데 이렇게 유지된 타입 인자는 mapper.readVAlue(this) 에서 어떻게 활용되고 있는 걸까? 제네릭 함수 호출시 타입인자를 명시적으로 지정하려면 아래와 같이 호출하려는 함수명과 인자 사이에 타입인자가 사용되어야 한다.

mapper.readValue<T>(this)

 

하지만 위 ObjectMapperUtil 에 포함된 toObject 함수에서는 리턴타입으로 T를 명시하고 있어서 컴파일러가 타입인자를 추론할 수 있기  때문에 함수 호출시 명시적으로 타입인자를 지정할 필요가 없다.

 

물론 명시적으로 타입인자를 지정해도 문제는 되지 않지만 이 경우 intellij 기준 오히려 아래와 어드바이스 문구를 확인할 수 있을 것이다. 번역기 돌려보면 "명시적 형식 인수 제거" 라는 뜻이다.

Remove explicit type arguments
728x90

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

코틀린 with, apply, also, let, run  (0) 2021.12.23
[Kotlin] interface default method  (0) 2021.12.22
[Kotlin] inline  (0) 2021.12.06
[Kotlin] 코틀린에서 유틸함수 만들기  (0) 2021.11.24
[Kotlin] val, const val  (0) 2021.11.22