Effective Kotlin - variance에 대하여

JINHO LEE·2024년 7월 8일
0

아이템 24: 제네릭 타입과 variance 한정자를 활용하라

Variance(변성)은 제네릭 타입 시스템에서 타입 간의 상속 관계를 정의하고, 이를 통해 안전하게 타입을 변환할 수 있도록 하는 개념이다.
Kotlin에서는 제네릭 타입의 변성을 다루기 위해 in과 out 키워드를 사용한다.
이를 각각 공변성(covariance)과 반공변성(contravariance)이라고 한다.
또한, 변성을 지정하지 않는 경우는 무변성(invariance)이라고 한다. 이 개념들을 자세히 살펴보자.

공변성(Covariance)

공변성은 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>로 안전하게 사용할 수 있다.

반공변성(Contravariance)

반공변성은 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>로 안전하게 사용할 수 있다.

무변성(Invariance)

무변성은 변성 한정자가 없는 상태이다. 무변성은 특정 타입이 다른 타입의 하위 타입이나 상위 타입으로 변환될 수 없음을 의미한다. 즉, 타입이 정확히 일치해야 한다.

class Box<T>(val value: T)

val stringBox: Box<String> = Box("Hello")
// val anyBox: Box<Any> = stringBox  // 오류 발생

위 예제에서 Box는 무변성으로 정의되었기 때문에, Box을 Box로 변환할 수 없다.

변성의 사용 사례

변성은 주로 다음과 같은 상황에서 유용하게 사용된다:

  • 공변성: 특정 타입의 읽기 전용 프로듀서(생산자)를 다룰 때 유용하다. List가 대표적인 예이다.
  • 반공변성: 특정 타입의 쓰기 전용 컨슈머(소비자)를 다룰 때 유용다. Comparator가 대표적인 예이다.
  • 무변성: 읽기와 쓰기 모두 가능한 경우 타입의 안전성을 보장하기 위해 사용한다.
    Kotlin에서 변성을 적절히 사용하면 타입 시스템의 유연성과 안전성을 모두 확보할 수 있다. 이를 통해 코드의 재사용성과 가독성을 높이는 데 큰 도움이 된다.

정리하자면,

변성 종류키워드설명예제
공변성(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>로 사용 불가

0개의 댓글