
지난 포스팅에 이어서
이번에는 Generic의 변성에 대해 알아보도록 하겠습니다.
개요
코틀린의 변성(Variance)은 제네릭 타입 간의 상속 관계를 정의하는 방식입니다.
변성은 무공변성(Invariance), 공변성(Covariance), 반공변성(Contravariance) 로 나뉩니다.
무공변성 (Invariance)
제네릭은 기본적으로 무변성입니다.
Box<SubType>은 Box<SuperType>으로 간주되지 않으며, 두 타입은 전혀 관련이 없습니다.
class Box<T>(val value: T)
fun useBox(box: Box<Any>) {
println(box.value)
}
fun main() {
val stringBox: Box<String> = Box("Hello")
useBox(stringBox) // 컴파일 오류
}

이유
무공변성은 타입 안정성을 보장합니다.
Box<Subtype>을 Box<SuperType>으로 허용한다면, 잘못된 타입을 넣는 상황이 발생할 수 있습니다.
그럼에도 불구하고,
객체 간의 상하 관계를 제네릭에서도 유지하고 싶다면 변성을 사용해야 합니다.
공변성 (Covariance)
기본 원칙
- 공변성은 하위 타입 관계를 유지합니다.
- Producer<out T>는 Producer<SubType>이 Producer<SuperType>으로 변환되도록 만듭니다.
- 읽기 전용 역할을 수행할 때 적합합니다.
선언 방법
- 제네릭 타입 매개변수 앞에 out 키워드를 추가합니다.
class Producer<out T>(val value: T) {
fun get(): T = value
}
fun main() {
val stringProducer: Producer<String> = Producer("Kotlin")
val anyProducer: Producer<Any> = stringProducer // 공변성 허용
println(anyProducer.get()) // Kotlin
}
out 키워드의 의미
- T는 반환값으로만 사용 가능하며, 입력값으로 사용될 수 없습니다.

제한 이유
반한된 값의 타입이 String에서 Any로 특정지어 사용해도 문제되지 않습니다.
반면, String이 사용하던 메서드를 Any에서 사용할 수 없을 수도 있습니다.
class Producer<out T>(private val value: T) {
fun get(): T = value
fun set(value: T) { T.length() } // 컴파일 에러: 쓰기를 허용하지 않음
}
fun main() {
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer // 공변성 허용
anyProducer.set(123) // 컴파일 에러
}
반공변성 (Contravariance)
기본 원칙
- 반공변성은 상위 타입 관계를 유지합니다.
- Consumer<in T>는 Consumer<SuperType>이 Consumer<SubType>으로 변환되도록 만듭니다.
- 쓰기 전용 역할을 수행할 때 적합합니다.
선언 방법
- 제네릭 타입 매개변수 앞에 in 키워드를 추가합니다.
class Consumer<in T> {
fun consume(value: T) {
println(value)
}
}
fun main() {
val anyConsumer: Consumer<Any> = Consumer()
val stringConsumer: Consumer<String> = anyConsumer // 반공변성 허용
stringConsumer.consume("Hello") // Hello
}
in 키워드의 의미
- T는 입력값으로만 사용 가능하며, 반환값으로 사용될 수 없습니다.

제한 이유
Any 타입이 사용하는 메서드는 String에서도 사용할 수 있습니다.
반면, 반환된 값이 String인지, Any의 (String이 아닌) 다른 하위 타입인지 알 수 없습니다.
class Consumer<in T> {
fun consume(value: T) { println(value) }
fun produce(): T {} // 컴파일 에러: 반환을 허용하지 않음
}
fun main() {
val anyConsumer: Consumer<Any> = Consumer<Any>()
val stringConsumer: Consumer<String> = anyConsumer // 반공변성 허용
val item: String = stringConsumer.produce() // String으로 간주할 수 없다
}
정리
무공변성, 공변성, 반공변성을 정리해보겠습니다.
class Box<T>(val value: T)
class Producer<out T>(val value: T)
class Consumer<in T>
무공변성 | val box: Box<Any> = Box("Hi") | ❌ 컴파일 에러 | Box<String>는 Box<Any>가 아님 |
공변성 (out) | val p: Producer<Any> = Producer("Hi") | ✅ 성공 | Producer<String>는 Producer<Any>로 변환 가능 |
반공변성 (in) | val c: Consumer<String> = Consumer<Any>() | ✅ 성공 | Consumer<Any>는 Consumer<String>로 변환 가능 |
실제 활용
1) 공변성 : 읽기 전용 컬렉션
List는 공변성을 가지므로, List<SubType>을 List<SuperType>으로 변환할 수 있습니다.
fun main() {
val strings: List<String> = listOf("A", "B", "C")
val anys: List<Any> = strings // 공변성 허용
println(anys) // [A, B, C]
}
2) 반공변성 : 이벤트 처리기
이벤트 처리와 같이 입력 전용으로 사용하는 경우, 반공변성을 활용할 수 있습니다.
class EventConsumer<in T> {
fun handleEvent(event: T) {
println("Handling event: $event")
}
}
fun main() {
val anyConsumer: EventConsumer<Any> = EventConsumer()
val stringConsumer: EventConsumer<String> = anyConsumer // 반공변성 허용
stringConsumer.handleEvent("User logged in")
}
3) 무공변성 : 읽기 및 쓰기 모두 가능
mutableList는 무공변성을 가지므로, 특정 타입에서만 안전하게 사용할 수 있습니다.
fun main() {
val strings: MutableList<String> = mutableListOf("A", "B")
val anys: MutableList<Any> = strings // ❌ 컴파일 에러: 무공변성
}
'Kotlin' 카테고리의 다른 글
[Kotlin] Builder Pattern 대체하기 (0) | 2025.01.13 |
---|---|
[Kotlin] Generic (3) - 그 외 (2) | 2024.12.19 |
[Kotlin] Generic (1) - 제네릭? (0) | 2024.12.10 |
[Kotlin] 위임 (Delegation) (2) | 2024.11.26 |
[Kotlin] DSL (Domain Specific Language) (0) | 2024.11.05 |