본문 바로가기

Kotlin

[Kotlin] Generic (2) - 변성

powered by pixabay

 

지난 포스팅에 이어서

이번에는 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' 카테고리의 다른 글