2021-01-01

공변성과 반공변성

공변성(covariant)을 얘기하기 전에 제네릭(Generic)에 대해서 간략하게 짚고 넘어가야 하는 게 있다. 제네릭은 런타임에 타입을 확정 짓는다.
written by

명시적이어야 하는 제네릭

공변성(covariant)을 얘기하기 전에 제네릭(Generic)에 대해서 간략하게 짚고 넘어가야 하는 게 있다. 제네릭은 런타임에 타입을 확정 짓는다. 컴파일 단계에서는 제네릭이 어떤 타입인지 알 수 없다는 것이다. 따라서, 제네릭은 엄격해야 한다. T라는 제네릭의 위치에 T의 서브 타입이 올 수는 없다. 이것에 중점을 두고 이해를 하자.

상속 관계인 클래스 간의 제네릭

아주 간단한 예시를 들어보자. Animal 클래스를 상속하는 Cat, Dog, Bird라는 클래스가 있다. Animal 클래스를 담는 배열을 만들고 Cat, Dog, Bird를 무작위로 넣었다고 가정해보자. 상속 관계가 있으므로 문제없이 들어갈 것이다.

하지만 이것은 우리가 상속 관계를 알고 있기 때문에 문제가 발생하지 않을 것을 아는 것이다. 제네릭은 런타임에 타입을 결정한다. 그렇기 때문에 컴파일 단계에서는 이들의 상속관계를 알 방법이 없다. 일단 들어는 왔는데 Animal의 Cat, Dog, Bird 중 무엇인지 모른다는 의미이다.

앞으로 타입을 알 수 없는 제네릭은 ?로 표시하겠다. 제네릭이 매개변수로 사용되면 소비(Consume)이라는 용어를 쓰고, 반환 타입으로 사용되면 생산(Produce)이라는 용어를 쓴다.

Kotlin Code

// Animal > Cat, Dog, Bird 관계인 클래스들.
open class Animal { }
class Cat: Animal() { }
class Dog: Animal() { }
class Bird: Animal() { }

// Animal 인스턴스를 담는 배열.
val animals = mutableListOf<Animal>()

// 컴파일 단계에서 Animal > Cat, Dog, Bird 포함 관계를 이미 알고 있으므로, 문제없이 삽입이 가능한 것을 알 수 있다.
animals.add(Cat())
animals.add(Dog())
animals.add(Bird())

// in T == ? super T를 의미한다.
// Animal > Cat, Dog, Bird 이지만 List<Animal> > List<Cat>, List<Dog>, List<Bird>는 아니다.
// 이제 List<?>에 List<Animal>을 담을 수 있게 in 키워드로 정의를 했다.
// 담는 쪽의 ? 제네릭은 반환값 T와 연결되어 있고, 넣는 쪽의 Animal 제네릭은 매개변수 t와 연결되어 있다. 
// ?는 Animal, Cat, Dog, Bird 클래스 중의 하나지만, 어떤 것일지는 알 수 없다.
class Consumer<in T: Animal> {

    // Consumer<Animal>().consume(t)에서 t는 확실하게 Animal 클래스에 대한 정보를 담고 있으므로 이것은 가능하다.
    // t가 어떤 클래스인지는 모르지만 Animal 클래스거나 Animal 클래스를 슈퍼 클래스로 가지는 것은 확실하기 때문이다.
    // t는 Animal, Cat, Dog, Bird 중 하나일 것이다.
    fun consume(t: T) { }

    // 제네릭은 명시적이어야 하기 때문에 확실하게 정해야 하는데, Consumer<Animal>().produce()에서 T를 반환해야 한다면
    // T는 Animal 클래스를 슈퍼 클래스로 가지는 것만 보장하고 Animal 클래스임을 보장하지는 않으므로 이것은 불가능하다.
    // Cat, Dog, Bird 중 어느 타입도 Animal 클래스를 슈퍼 클래스로 가지고 있기 때문이다.
    // fun produce(): T = Cat() as T
}

// 이 코드는 문제없이 동작한다.
val consumes: List<Consumer<Cat>> = listOf(Consumer<Animal>())
// 이 코드는 동작하지 않는다.
// val consumes: List<Consumer<Animal>> = listOf(Consumer<Cat>())

// out T == ? extends T를 의미한다.
// Animal > Cat, Dog, Bird 이지만 List<Animal> > List<Cat>, List<Dog>, List<Bird>는 아니다.
// 이제 List<Animal>에 List<?>을 담을 수 있게 out 키워드로 정의를 했다.
// 담는 쪽의 Animal 제네릭은 반환값 T와 연결되어 있고, 넣는 쪽의 ? 제네릭은 매개변수 t와 연결되어 있다.
// ?는 Animal, Cat, Dog, Bird 클래스 중의 하나지만, 어떤 것일지는 알 수 없다.
class Producer<out T: Animal> {

    // Producer<?>().consume(t)에서 t는 Animal 클래스를 상속하는 어떤 클래스인지 알 수가 없다.
    // t는 Animal, Cat, Dog, Bird 중 하나일 것이다. 따라서 이것은 불가능하다.
    // fun consume(t: T) { }

    // 제네릭은 명시적이어야 하기 때문에 확실하게 정해야 하는데, Producer<?>().produce()에서 T를 반환해야 한다면
    // T는 Animal 클래스를 상속하는 것을 보장하므로 Animal 클래스로 반환이 가능함을 알 수 있다.
    fun produce(): T = Cat() as T
}

// 이 코드는 문제없이 동작한다.
val producers: List<Producer<Animal>> = listOf(Producer<Cat>())
// 이 코드는 동작하지 않는다.
// val producers: List<Producer<Cat>> = listOf(Producer<Animal>())
2021 © Cinntiq's Studio