변성(variance)의 이해

sanghoon·2020년 12월 5일
0
post-thumbnail

변성이란 제네릭에서 형식 매개변수가 클래스 계층에 어떤 영향을 미치는지를 말합니다. 이는 다음과 같은 세 유형으로 나눌 수 있습니다.

T가 T`의 상위 자료형이라 할 때

코틀린은 무변성을 디폴트로 합니다. 다음과 같은 예외가 발생할 수 있기 때문입니다.

    val strs: MutableList<String> = mutableListOf()
    
    val objs: MutableList<Any> = strs 
    // !!! A compile-time error here saves us from a runtime exception later

    objs.add(1) // Here we put an Integer into a list of Strings

    val s: String = strs[0] // !!! ClassCastException: Cannot cast Integer to String

위 코드에서 MutableList<String>이 MutableList<Any>의 하위 자료형이었다면, 네번째 코드에서 Int형을 String으로 캐스팅할 수 없기 때문에 예외가 발생합니다. 따라서 run-time safety를 보장하기 위해 무변성을 기본으로 하는 것입니다.

그러나 무변성만을 원칙으로 한다면 공변과 반공변이 필요한 상황에서 코드를 작성하려면 타입 캐스트 등에 사용되는 불필요한 코드가 길어지기 때문에, 코틀린에서는 out과 in 키워드를 제공해 이를 해결하고 있습니다.

[그림] 공변과 반공변의 예시

코드를 통해 out과 in 사용 시 주의사항을 알아봅시다.

out 키워드를 사용하면 다음 코드의 주석에서 표시한 것과 같이 in 위치(T 타입을 인자로 넣는 것)에 작성하는 것을 금지하고 있습니다.

class Producer<out T: Music>(private var piece: T) {
    fun producePiece(): T = piece
  //fun setPiece(piece: T) { !! out으로 선언된 T는 in 위치에 나타날 수 없습니다 !!
  //    this.piece = piece
  //}
}

open class Music

class Metal : Music()

fun main() {
    val soundOfMusic = Music()

    val ultraMen = Metal()

    var person1 = Producer<Music>(soundOfMusic)
    var person2 = Producer<Metal>(ultraMen)

    // person2 = person1  !! type mismatch !!
}

만약 in 위치에서 사용할 수 있게 되면 person2.setPiece(soundOfMusic)과 같은 코드가 작성이 가능해져 Producer이 soundOfMusic의 생산자가 되어버리는 이상한 현상이 발생하기 때문입니다.

반대로 in 키워드를 사용하면 out의 위치에서 T가 사용되는 것(T 타입과 그 상위 자료형을 return하는 것)이 금지됩니다.

class Consumer<in T>(private var source: T) {
  //fun produceSource(): T = source !! in으로 선언된 T는 out 위치에 나타날 수 없습니다 !!
    fun setSource(source: T) {
        this.source = source
    }
}

open class Vegetable

class Bamboo : Vegetable()

fun main() {
    val broccoli = Vegetable()

    val longBamboo = Bamboo()

    var vegeConsumer = Consumer<Vegetable>(broccoli)
    var bamboConsumer = Consumer<Bamboo>(longBamboo)

    vegeConsumer.setSource(longBamboo) // vegeConsumer can consume longBamboo
    //panda.setSource(broccoli) !! type mismatch !!
    
}

Consumer 객체는 T 타입을 소비만 할 수 있을 뿐 생산할 수는 없기 때문입니다.

사실 위 [그림]에서도 보듯이 실생활에서는 "어떻게 consumer of vegetalbe이 consumer of bamboo의 하위 자료형이 될 수 있느냐"와 같은 의문이 들 수 있습니다.

그러나 하위 자료형을 상위 자료형의 특별 케이스라 생각하는 것이 아니라, 하위 자료형이 상위 자료형을 상속받아 하위 자료형에서 상위 자료형의 메서드와 프로퍼티를 받아들인다고 생각해보면 이해하실 수 있을 것입니다.


이 글은 밑 주소에 나온 글을 참고해 작성되었습니다.

ref1 : https://kotlinlang.org/docs/reference/generics.html
ref2 : https://proandroiddev.com/understanding-type-variance-in-kotlin-d12ad566241b

제가 이해한 것을 바탕으로 작성한 글입니다. 사실과 다를 수 있음을 명시합니다.

댓글을 통한 피드백은 언제나 환영입니다 ㅎㅎ

0개의 댓글