타입 시스템 (Variance, Contravariance) - swkim0128/PARA GitHub Wiki
코틀린의 타입 시스템에서는 제네릭(Generic)을 사용할 때 타입의 변환(업캐스팅, 다운캐스팅)이 가능하도록 특정 키워드를 제공한다. 이러한 타입 변환 방식은 공변성(Covariance), 반공변성(Contravariance), 무공변성(Invariance)**으로 구분된다.
타입 변환(Variance)이 필요한 이유
- 제네릭 클래스 간의 타입 변환을 안전하게 보장하기 위해 필요하다.
- 함수의 매개변수와 반환 타입을 올바르게 다루기 위해 사용된다.
기본적으로 제네릭 타입은 변환이 불가능하다.
예제: 일반적인 제네릭 클래스
class Box<T>(val value: T)
fun main() {
val stringBox: Box<String> = Box("Hello")
// val anyBox: Box<Any> = stringBox ❌ 컴파일 오류 발생 (타입 변환 불가)
}
왜 오류가 발생하는가?
- Box<T>는 기본적으로 무공변적(Invariant)이므로 **Box<String>을 Box<Any>로 변환할 수 없다.
- 해결 방법: 공변성(out) 또는 반공변성(in) 키워드 사용.
공변성이란 제네릭 타입이 상위 타입으로 변환(업캐스팅)될 수 있도록 허용하는 것이다. 코틀린에서는 out 키워드를 사용하여 공변성을 적용할 수 있다.
공변성 예제
interface Producer<out T> {
fun produce(): T
}
class StringProducer : Producer<String> {
override fun produce(): String = "Hello"
}
fun main() {
val stringProducer: Producer<String> = StringProducer()
val anyProducer: Producer<Any> = stringProducer // ✅ 업캐스팅 허용
println(anyProducer.produce()) // 출력: Hello
}
- Producer<out T> → T는 공변성이 적용된 타입이다.
- Producer<String>을 Producer<Any>로 변환할 수 있다.
- out 키워드를 사용하면, 해당 타입은 “읽기 전용”이 된다.
- 즉, 반환(return)은 가능하지만, 값을 변경(setter)은 불가능하다.
- out을 사용하면 반환(return)은 가능하지만, 매개변수로 사용할 수 없다.
- 예제:
interface Producer<out T> {
fun produce(): T
// fun consume(item: T) ❌ 컴파일 오류 발생 (매개변수 사용 불가)
}
✅ 공변성을 사용하는 대표적인 예:
- List<T> → List<String>을 List<Any>로 변환 가능.
반공변성이란 제네릭 타입이 하위 타입으로 변환(다운캐스팅)될 수 있도록 허용하는 것이다. 코틀린에서는 in 키워드를 사용하여 반공변성을 적용할 수 있다.
반공변성 예제
interface Consumer<in T> {
fun consume(item: T)
}
class AnyConsumer : Consumer<Any> {
override fun consume(item: Any) {
println("Consumed: $item")
}
}
fun main() {
val anyConsumer: Consumer<Any> = AnyConsumer()
val stringConsumer: Consumer<String> = anyConsumer // ✅ 다운캐스팅 허용
stringConsumer.consume("Hello") // 출력: Consumed: Hello
}
- Consumer<in T> → T는 반공변성이 적용된 타입이다.
- Consumer<Any>을 Consumer<String>으로 변환할 수 있다.
- in 키워드를 사용하면, 해당 타입은 “쓰기 전용”이 된다.
- 즉, 매개변수(parameter)로는 사용할 수 있지만, 반환(return)은 불가능하다.
- in을 사용하면 반환(return)은 불가능하다.
- 예제:
interface Consumer<in T> {
fun consume(item: T)
// fun produce(): T ❌ 컴파일 오류 발생 (반환 불가능)
}
✅ 반공변성을 사용하는 대표적인 예:
- Comparator<T> → Comparator<Any>을 Comparator<String>으로 변환 가능.
T를 in과 out 동시에 사용할 수 없다.
class Container<T>(var item: T) // ❌ 컴파일 오류 발생
이유:
- in → 매개변수로 사용 (쓰기 가능)
- out → 반환값으로 사용 (읽기 가능)
- 하지만 var item: T는 읽기와 쓰기가 모두 가능하므로 in과 out을 동시에 사용할 수 없다.
✅ 대안: invariant(무공변성)를 유지하거나, 별도의 타입을 분리해야 한다.
개념 | 키워드 | 변환 방향 | 사용 위치 | 예제 |
---|---|---|---|---|
공변성 | out | Producer → Producer (업캐스팅) | 반환 타입으로 사용 가능 (fun produce(): T) | List |
반공변성 | in | Consumer → Consumer (다운캐스팅) | 매개변수 타입으로 사용 가능 (fun consume(item: T)) | Comparator |
무공변성 | 없음 | 변환 불가능 | 읽기/쓰기 둘 다 가능 (var item: T) | MutableList |
- List<T>는 읽기 전용이므로 out 키워드가 적용된다.
val strings: List<String> = listOf("Kotlin", "Java")
val anyList: List<Any> = strings // ✅ 업캐스팅 가능 (공변성 적용)
- MutableList<T>는 읽기/쓰기 둘 다 가능하므로 in과 out을 사용할 수 없다.
val strings: MutableList<String> = mutableListOf("Kotlin", "Java")
// val anyList: MutableList<Any> = strings ❌ 변환 불가능 (무공변성)
- Comparator<T>는 비교할 때 매개변수로 T를 사용하므로 in 키워드를 사용할 수 있다.
val anyComparator: Comparator<Any> = Comparator { a, b -> a.hashCode() - b.hashCode() }
val stringComparator: Comparator<String> = anyComparator // ✅ 다운캐스팅 가능 (반공변성 적용)
- 기본적으로 제네릭 타입은 변환이 불가능(무공변성)하다.
- out을 사용하면 공변성을 제공하여 상위 타입으로 변환 가능.
- in을 사용하면 반공변성을 제공하여 하위 타입으로 변환 가능.
- 읽기 전용(Producer)에는 out, 쓰기 전용(Consumer)에는 in을 사용해야 한다.
- List<T>는 out을, Comparator<T>는 in을 사용한다.
💡 제네릭 타입을 설계할 때는 변환 방향을 고려하여 out과 in을 적절히 사용해야 한다! 🚀