kyucumber
전체 글 보기

이펙티브 코틀린 아이템 24. 제네릭 타입과 variance 한정자를 활용하라

invariant

variacne 한정자(out 또는 in)이 없는 경우 invariant(불공변성)는 제네릭 타입으로 만들어지는 타입들이 서로 관련성이 없다는 의미이다.

class Cup<T> // 아래 세가지 다 독립적이며 어떠한 관련성도 없다. val intCup: Cup<Int> val intCup: Cup<Any> val intCup: Cup<Nothing>

variance

제네릭 타입 간 관련성을 가지길 원한다면 out 또는 in이라는 variance 한정자를 붙일 수 있다.

covariant(공변성)과 out 한정자

out은 타입 파라미터를 covariant(공변성)으로 만든다.

class Cup<out T> open class Dog class Puppy: Dog() val b: Cup<Dog> = Cup<Puppy>() // OK val a: Cup<Puppy> = Cup<Dog>() // 오류

A가 B의 서브타입일 때 Cup<A>Cup<B>의 서브타입이라는 의미, 위에서는 Cup<Dog>에 서브타입인 Cup<Puppy>가 할당될 수 있다.

contravariant(반변성)과 in 한정자

in은 out과 반대 의미이다. in 한정자는 타입 파라미터를 contravariant(반변성)으로 만든다.

class Cup<in T> open class Dog class Puppy: Dog() val b: Cup<Dog> = Cup<Puppy>() // 오류 val a: Cup<Puppy> = Cup<Dog>() // OK

A가 B의 서브타입일 때 Cup<A>Cup<B>의 슈퍼타입이라는 의미, 위에서는 Cup<Puppy>에 슈퍼타입인 Cup<Dog>가 할당될 수 있다.

variance 한정자를 그림으로 나타내면 다음과 같다.

item-24-variance.png

함수 타입

함수 타입은 파라미터 유형과 리턴 타입에 따라 서로 관계를 갖는다.

(Int) → Any 타입의 함수는 다음과 같은 타입 등으로도 동작한다.

  • (Int) → Number
  • (Number) → Any
  • (Number) → Number
  • (Number) → Int

이는 아래와 같은 관계에 의해서 가능하다.

item-24-variance.png

파라미터 타입은 Int → Number → Any로 높은 타입으로 이동하며, 리턴 타입은 Any → Number → Int로 낮은 타입으로 이동한다.

코틀린의 함수 타입의 모든 파라미터 타입은 contravariant(in 한정자)이며 모든 리턴 타입은 covariant(out 한정자)이다.

함수 타입을 사용할때는 이처럼 자동으로 variance 한정자가 사용되며, 코틀린에서 자주 사용되는 것으로는 covariant(out 한정자)를 가진 List가 존재한다.

variance 한정자의 안정성

자바의 배열은 covariant이며 이로 인해 아래와 같은 문제가 발생할 수 있다.

Integer[] numbers = {1, 4, 2, 1}; Object[] objects = numbers; objects[2] = "B" // 오류

위는 컴파일 타임 중에는 아무 문제가 없지만 런타임 오류가 발생한다.

코틀린은 이러한 결함을 해결하기 위해 Array를 invariant(IntArray, CharArray, ByteArray 등) 만들었으며, Array<Int>Array<Any> 등으로 바꿀 수 없다.

covariant 타입 파라미터와 함수 타입의 파라미터

아래 코드는 covariant하지 않다. covariant 타입 파라미터(out)가 in 한정자 위치(함수의 타입 파라미터)에 있다면 covariant(out)와 업캐스팅을 연결해서 우리가 원하는 타입을 아무것이나 전달할 수 있다.

open class Dog class Puppy: Dog() class Hound: Dog() fun takeDog(dog: Dog) {} fun main() { takeDog(Dog()) takeDog(Puppy()) takeDog(Hound()) } class Box<out T> { private var value: T? = null // Type parameter T is declared as 'out' but occurs in 'in' position in type T fun set(value: T) { this.value = value } fun get(): T = value ?: error("Value not set") } fun main() { val puppyBox = Box<Puppy>() val dogBox: Box<Dog> = puppyBox dogBox.set(Hound()) // Puppy를 위한 Box지만 Hound를 set 함수에 전달할 수 있다. }

위와 같은 행위가 가능하다면 오류가 발생할 것이므로 코틀린은 public in 한정자 위치(함수의 타입 파라미터)에 covariant 타입 파라미터(out)가 오는 것을 금지해 이런 오류를 사전에 방지한다.

주석에도 표시해 뒀지만, 위와 같이 클래스를 정의하면 아래와 같은 에러 메시지가 노출된다.

Type parameter T is declared as ‘out’ but occurs in ‘in’ position in type T

class Box<out T> { private var value: T? = null private fun set(value: T) { this.value = value } fun get(): T = value ?: error("Value not set") }

private으로 제한하면 오류가 발생하지 않는다. get 메소드의 리턴 타입에 T가 사용되는데 covariant(out 한정자)는 public out 한정자 위치에도 안전하므로 이는 따로 제한되지 않는다.

위와 같은 covariant는 Producer나 Immutable 데이터 홀더에 많이 사용되며 아래 List<T>도 covariant의 형태를 띄고 있다.

public interface List<out E> : Collection<E> {

MutableList<T>의 T는 in 한정자 위치(함수의 타입 파라미터)에서 사용되며 안전하지 않으므로 invariant로 제공된다.

public interface MutableList<E> : List<E>, MutableCollection<E> {

이외에 좋은 예로는 Response가 있다.

sealed class Response<out R, out E> class Failure<out E>(val error: E): Response<Nothing, E>() class Success<out R>(val value: R): Response<R, Nothing>()

covariant와 Nothing 타입으로 인해 Failure는 오류 타입을 지정하지 않아도 되고, Success는 잠재적인 값을 지정하지 않아도 된다.

contravariant 타입 파라미터와 함수 리턴 타입, 클래스의 프로퍼티 타입

contravariant 타입 파라미터(in)와 public out 위치(함수 리턴 타입 또는 클래스의 프로퍼티 타입)에서도 문제가 발생할 수 있다.

open class Car interface Boat class Amphibious: Car(), Boat fun getAmphibious(): Amphibious = Amphibious() class Box<in T>( // Type parameter T is declared as 'in' but occurs in 'out' position in type T val value: T ) fun main() { val garage: Box<Car> = Box(Car()) val amphibious: Box<Amphibious> = garage val boat: Boat = amphibious.value // 실제로는 Car를 위한 Box이다. // 책에는 garage.value로 되어있는데 조금 이상해서 수정했습니다. }

Box에 어떤 타입이 들어있는지 확실하게 알수가 없다.

value에 T 타입을 지정하면 아래와 같은 에러 메시지가 노출된다.

Type parameter T is declared as ‘in’ but occurs in ‘out’ position in type T

이러한 상황을 막기 위해 코틀린은 contravariant 타입 파라미터(in)을 public out 한정자 위치(함수 리턴 타입, 클래스의 프로퍼티 타입)위치에 사용하는것을 금지하고 있다.

물론 이번에도 아래처럼 private로 지정하면 아무런 문제가 없다.

class Box<in T>( private val value: T )

contravariant를 사용하는 예로는 kotlin.coroutine.Continuation 이 있다.

public interface Continuation<in T> { /** * The context of the coroutine that corresponds to this continuation. */ public val context: CoroutineContext /** * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the * return value of the last suspension point. */ public fun resumeWith(result: Result<T>) }

variance 한정자의 위치

variance 한정자는 선언 부분과 클래스 인터페이스를 활용하는 위치에 사용할 수 있다.

  • 선언 부분

    일반적으로 이 위치에 사용하며, 클래스와 인터페이스가 사용되는 모든 곳에 영향을 준다.

    // 선언 쪽의 variance 한정자 class Box<out T>(val value: T)
  • 클래스와 인터페이스를 활용하는 위치

    이 위치에 variance를 적용하면 특정 변수에만 variance 한정자가 적용된다. 특정 인스턴스에만 적용해야 하는 경우 사용한다.

    class Box<T>(val value: T) // 사용하는 쪽의 variance 한정자 val boxAny: Box<out Any> = boxStr

MutableList에 in 한정자를 포함하면 요소의 리턴이 불가능하므로 붙이지 않는다.

하지만 여러 타입을 받아들이게 하기 위해 아래와 같이 단일 파라미터 타입에 in을 붙여 활용할 수 있다.

interface Dog interface Cutie data class Puppy(val name: String): Dog, Cutie data class Hound(val name: String): Dog data class Cat(val name: String): Cutie fun fillWithPuppies(list: MutableList<in Puppy>) { list.add(Puppy("One")) list.add(Puppy("Two")) } fun main() { val dogs = mutableListOf<Dog>(Hound("Pluto")) fillWithPuppies(dogs) val cats = mutableListOf<Cutie>(Cat("Pluto")) fillWithPuppies(cats) }

variance 한정자를 사용하면 아래처럼 제한이 있을 수 있다.

MutableList<out T> get() 요소 추출 시 T 타입이 리턴되지만 set은 Nothing 타입의 아규먼트가 전달될 수도 있을것으로 예상되므로 사용할 수 없다.

MutableList<in T> 사용 시 get과 set을 모두 사용할 수 있지만 get()을 사용할 경우 Any?가 리턴된다. 이는 슈퍼타입을 가진 리스트가 존재할 가능성이 있기 때문이다.

정리

코틀린은 강력한 제네릭 기능을 제공하며 아래와 같은 타입 한정자가 존재한다.

  • 기본적인 variance의 동작은 invariant이다. Cup<A>Cup<B>는 기본적으로는 아무런 관계를 갖지 않는다.
  • out 한정자는 타입 파라미터를 covariant하게 만든다. covariant 타입은 out 위치에 사용할 수 있다.
  • in 한정자는 타입 파라미터를 contravariant하게 만든다. contravariant 타입은 in 위치에 사용할 수 있다.
  • List, Set의 타입 파라미터는 covariant(out) 이다.
  • Array, MutableList, MutableSet의 타입 파라미터는 invaraint이다.
  • 함수 타입의 파라미터 타입은 contravariant(in)이다.
  • 리턴 타입의 파라미터 타입은 covariant(out) 이다. 책에는 contravariant로 되어 있음 임의로 수정
  • 리턴만 되는 타입에는 covariant(out)을 사용한다.
  • 허용만 되는 타입에는 contravariant(in)을 사용한다.

Reference

  • 이펙티브 코틀린 - 프로그래밍 인사이트, 마르친 모스칼라 지음, 윤인성 옮김

개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.

오류가 있다면 댓글을 남겨주세요.