아이템 24: 제네릭 타입과 variance 한정자를 활용하라
Variance(변성)은 제네릭 타입 시스템에서 타입 간의 상속 관계를 정의하고, 이를 통해 안전하게 타입을 변환할 수 있도록 하는 개념이다.
Kotlin에서는 제네릭 타입의 변성을 다루기 위해 in과 out 키워드를 사용한다.
이를 각각 공변성(covariance)과 반공변성(contravariance)이라고 한다.
또한, 변성을 지정하지 않는 경우는 무변성(invariance)이라고 한다. 이 개념들을 자세히 살펴보자.
공변성은 out 키워드를 사용하여 정의됩니다.
공변성은 특정 제네릭 타입이 그 타입의 하위 타입을 허용할 수 있음을 의미한다.
예를 들어 Producer<out T>
는 Producer<T>
가 Producer<S>
의 하위 타입일 때, T가 S의 하위 타입이라면 안전하게 Producer<S>
로 사용할 수 있음을 보장합니다.
interface Producer<out T> {
fun produce(): T
}
fun printProducers(producers: List<Producer<Any>>) {
producers.forEach { println(it.produce()) }
}
val stringProducer: Producer<String> = object : Producer<String> {
override fun produce(): String = "Hello"
}
val anyProducer: Producer<Any> = stringProducer // T가 String에서 Any로 변성 가능
printProducers(listOf(anyProducer))
위 예제에서 Producer<out T>
는 공변성으로 정의되었기 때문에, Producer<String>
을 Producer<Any>
로 안전하게 사용할 수 있다.
반공변성은 in 키워드를 사용하여 정의된다. 반공변성은 특정 제네릭 타입이 그 타입의 상위 타입을 허용할 수 있음을 의미한다. 예를 들어, Consumer<in T>
는 Consumer<T>
가 Consumer<S>
의 상위 타입일 때, T가 S의 하위 타입이라면 안전하게 Consumer<S>
로 사용할 수 있음을 보장한다.
interface Consumer<in T> {
fun consume(item: T)
}
fun addConsumers(consumers: MutableList<Consumer<String>>) {
consumers.add(object : Consumer<String> {
override fun consume(item: String) {
println("Consumed: $item")
}
})
}
val anyConsumer: Consumer<Any> = object : Consumer<Any> {
override fun consume(item: Any) {
println("Consumed: $item")
}
}
val stringConsumer: Consumer<String> = anyConsumer // T가 Any에서 String으로 변성 가능
addConsumers(mutableListOf(stringConsumer))
위 예제에서 Consumer<in T>
는 반공변성으로 정의되었기 때문에, Consumer<Any>
를 Consumer<String>
로 안전하게 사용할 수 있다.
무변성은 변성 한정자가 없는 상태이다. 무변성은 특정 타입이 다른 타입의 하위 타입이나 상위 타입으로 변환될 수 없음을 의미한다. 즉, 타입이 정확히 일치해야 한다.
class Box<T>(val value: T)
val stringBox: Box<String> = Box("Hello")
// val anyBox: Box<Any> = stringBox // 오류 발생
위 예제에서 Box는 무변성으로 정의되었기 때문에, Box을 Box로 변환할 수 없다.
변성은 주로 다음과 같은 상황에서 유용하게 사용된다:
정리하자면,
변성 종류 | 키워드 | 설명 | 예제 |
---|---|---|---|
공변성(Covariance) | out | 특정 제네릭 타입이 그 타입의 하위 타입을 허용. 주로 읽기 전용 프로듀서에 사용 | Producer<out T> : Producer<String> 을 Producer<Any> 로 사용 가능 |
반공변성(Contravariance) | in | 특정 제네릭 타입이 그 타입의 상위 타입을 허용. 주로 쓰기 전용 컨슈머에 사용 | Consumer<in T> : Consumer<Any> 를 Consumer<String> 로 사용 가능 |
무변성(Invariance) | 없음 | 특정 타입이 다른 타입으로 변환 불가. 타입이 정확히 일치해야 함 | Box<T> : Box<String> 을 Box<Any> 로 사용 불가 |