본문 바로가기
코틀린

코틀린에서 변성(variance)이란 무엇인가 - 상세 설명

by 기계공학 주인장 2023. 2. 18.
반응형

변성(variance)이란?

  • 기저 타입(base type)이 같고 타입 인자(type argument)가 다른 경우 서로 어떤 관계가 있는지 설명하는 개념
  • 아래의 코드에서 List는 기저 타입 / <String>, <Any>는 타입 인자이다
List<String>, List<Any>

 

  • 단, 여기서 String은 Any의 하위 타입이지만 List<String>은 List<Any>의 하위 타입이 아니다

그렇기 때문에 다음과 같은 코드는 에러가 발생한다

fun addStringList(list: MutableList<Any>) {
    list.add("text")
}

fun addNumberList() {
    val numbers = mutableListOf(1, 2, 3)
    // 에러 발생
    addStringList(numbers)
}
  • 만약 위 코드가 에러가 발생하지 않을 경우 list에 의도하지 않은 타입 인자가 add 될 수 있다.
  • 즉, 겉으로 보기에는 상위-하위 타입처럼 보이지만 실제로는 서로 아무런 관계가 없는 타입을 무공변(invariant)이라고 한다

⇒ 이러한 무공변가변 컬렉션에서만 발생한다

 

그렇기 때문에 불변 컬렉션은 무공변이 일어나지 않는다

fun addNumberList() {
    val numbers =mutableListOf(1, 2, 3)
    // MutableList<Any> - error
    addStringMutableList(numbers)

    // List<Any> - success
    val anyList =listOf<Any>(0, "2")
}

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


왜 불변 / 가변 컬렉션에 따라 공변 / 무공변이 다르게 일어날까?

  • 먼저 모든 제네릭 타입은 다음과 같이 세 가지가 존재한다
  1. T 타입의 값을 반환하는 연산만 제공하고 T 타입의 값을 입력으로 받는 연산은 제공하지 않는 제네릭 타입 생산자 = 즉, 제네릭 타입이 생산자 역할만 할 때
  2. T 타입의 값을 입력으로 받기만 하고 절대 T 타입의 값을 반환하지 않는 제네릭 타입 소비자 = 제네릭 타입이 소비자 역할만 할 때
  3. 위 두가지에 포함되지 않는 타입
    • 마지막의 경우 (즉, 생산도 소비도 아닌 타입)은 타입 안전성을 깨지 않고는 하위 타입 관계를 유지할 수 없다
    • 그 이유는 위에서 언급했다시피 하위 값을 모두 넣을 수 있다면 가변 컬렉션을 활용하기 어렵기 때문이다.

예를 들면 다음과 같다

fun test() {
    val stringNode = TreeNode("Hello")
    // 여기서 공변성 때문에 에러 발생
    val anyNode: TreeNode<Any> = stringNode
    // 값을 받는 행위 자체에는 에러가 발생하지 않는다
    anyNode.addChild(123)
}

 

 

그렇다면 왜 List<T> 같은 불변 타입은 공변적일까?

불변 컬렉션은 T 타읩 값을 만들어내기만 하고 소비하지 않는다.

 

그렇기 때문에 List<Any>로 정의했다면 그냥 Any로 돌려줄 뿐이다.

 

그래서 대부분의 불변 컬렉션은 공변성을 유지한다.


in, out 프로젝션으로 공변성, 반공변성 부여하기

  • 위 예시에서 봤듯이 Int는 Number를 상속하고 Int의 Super class는 Number이지만 Generic에서는 상속관계가 성립하지 않는 다는 것을 알았다. = 불변성
  • 일반적인 사용에선 Int는 Number를 상속한다 = 공변성

out 프로젝션

out 프로젝션을 사용하면 불변성 -> 공변성 으로 바꾸는 것이 가능하다.

class Rectangle<T: Number>(val width: T, val height: T) {
}

fun main(args: Array<String>) {
  // 제네릭에 Double을 선언
  val derivedClass = Rectangle<Double>(10.5, 20.5)
  // 이를 제네릭에 Number를 선언한 객체에 대입하면 에러 발생
  val baseClass : Rectangle<Number> = derivedClass
}

위와 같은 예시는 에러가 발생한다.

 

하지만, out을 사용하면 공변성으로 바꿀 수 있다.

// out을 사용하여 공변성을 갖도록 명령한다.
class Rectangle<out T: Number>(val width: T, val height: T) {
}

fun main(args: Array<String>) {
  val derivedClass = Rectangle<Double>(10.5, 20.5)
  // Rectangle<Double>가 Rectangle<Number>의 하위 클래스라고 인식
  // 그래서 에러가 발생하지 않는다.
  val baseClass : Rectangle<Number> = derivedClass
}

 

in 프로젝션

in은 out의 반대입니다.

즉, 두 타입의 관계를 반대로 만들 수 있습니다.

 

예를 들면 위에선 

 

Rectangle<Double>가 Rectangle<Number>의 하위 클래스였지만 

 

in 프로젝션을 사용하면

 

 Rectangle<Number>가 Rectangle<Double>의 하위 클래스가 될 수 있습니다.

// in을 사용하여 클래스 구조를 역전시킨다.
class Rectangle<in T: Number>(val width: T, val height: T) {
}

fun main(args: Array<String>) {
    val baseClass = Rectangle<Number>(10.5, 20.5)
    // Number 클래스가 Double로 들어갈 수 있게 된다
    val derivedClass : Rectangle<Double> = baseClass
}

이를 반공변성(Contravariance)라고 합니다.


요약

  • 변성이란 - 기저 타입(base type)이 같고 타입 인자(type argument)가 다른 경우 서로 어떤 관계가 있는지 설명하는 개념
  • 불변 / 가변 컬렉션에 따라 왜 공변, 무공변이 다르게 나타나는 이유는 제네릭의 역할에 따라 다르다
  • in, out 프로젝션으로 공변성, 반공변성을 자유롭게 부여할 수 있다

공변성, 반공변성, 불변성의 관계

 

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글