Kotlin

[Effective Kotlin] 22 - 일반적인 알고리즘을 구현할 때 제너릭을 사용하라

매운돌 2023. 6. 24. 18:06

Argument로 함수에 값을 전달할 수 있는 것처럼, 타입 Argument를 사용하면 함수에 타입을 전달할 수 있습니다. 그리고 타입 Argument를 사용하는 함수를 제너릭 함수라고 부릅니다. 대표적인 예로 stdlib에 있는 filter라는 함수가 있습니다.

 

타입 파리미터는 컴파일러에 타입과 관련된 정보를 제공하여 컴파일러가 타입을 조금이라도 정확하게 추측할 수 있게 해줍니다. 따라서 프로그램이 조금 더 안전해지고, 개발자는 프로그래밍이 편해집니다.

참고로, 타입 파라미터를 사용하면 개발자는 여러 가지 이득을 얻지만, 프로그램은 실질적인 이득이 없습니다. JVM바이트 코드의 제한으로 인해, 컴파일 시점에 제너릭과 관련된 정보는 사라집니다. 따라서 어떠한 이득도 얻을 수 없습니다.

예를 들어 filter 함수에서 람다 표현석 내부를 생각해 봅십니다. 컴파일러가 아규먼트가 컬렉션의 요소와 같은 타입이라는 것을 알 수 있으므로, 잘못 처리하는 것을 막을 수 있습니다. 또한 IDE도 이를 기반으로 여러 가지 유용한 제안을 해 줍니다. 

 

제너릭은 기본적으로 List<String> 또는 Set<User>처럼 구체적인 타입으로 컬렉션을 만들 수 있게 클래스와 인터페이스에 도입된 기능입니다. 물론 위에서 이야기 했듯이 컴파일 과정에서 최종적으로 이러한 타입 정보는 사라지지만, 개발 중는 특정 타입을 사용하게 강제할 수 있습니다.

이러한 타입 정보 덕분에 MutableList<Int>에 안전하게 Int를 추가할 수 있습니다. 또한 Set<User>에서 요소를 꺼내면, 그것이 User라는 것을 알 수 있습니다.

 

제너릭 제한

타입 파라미터의 중요한 기능 중 하나는 구체적인 타입의 서브타입만 사용하게 타입을 제한하는 것입니다. 아래의 코드는 콜론 뒤에 슈퍼타입을 설정해서 제한을 걸었습니다.

fun <T : Comparable<T>> Iterable<T>.sorted(): List<T> {
	/*...*/
}

fun <T, C : MutableCollection<in T>>
Iterable<T>.toCollection(destination: C): C {
	/*...*/
}

class ListAdapter<T: ItemAdapter>(/*...*/) { /*...*/ }

타입에 제한이 걸리므로, 내부에서 해당 타입이 제공하는 메서드를 사용할 수 있습니다. 예를 들어 T를 Iterable<Int>의 서브타입으로 제공하면, T타입을 기반으로 반복 처리가 가능하고, 반복 처리 때 사용되는 객체가 Int라는 것을 알 수 있습니다.

많이 사용하는 제한으로 Any가 있습니다. 이는 nullable이 아닌 타입을 나타냅니다.

또한 드물지만 아래와 같이 둘 이상의 제한을 걸 수도 있습니다.

fun <T: Animal> pet(animal: T) where T: GoodTempered {
	/*...*/
}

// 또는 

fun <T> pet(animal: T) where T: Animal, T: GoodTempered {
	/*...*/
}

 

정리

Kotlin 자료형 시스템에서 타입 파라미터는 굉장히 중요한 부분입니다. 일반적으로 이를 사용해서 type-safe 제너릭 알고리즘과 제너릭 객체를 구현합니다.