[Effective Kotlin] 아이템 24 : 제너릭 타입과 variance 한정자를 활용하라

0

다음과 같은 제너릭 클래스가 있습니다.

class Cup<T>

위에서 타입 파라미터 T는 variance 한정자가 없기 때문에 기본적으로 invariant입니다.

invariant라는것은 제너릭으로 만들어지는 타입들이 서로 관련성이 없다는것을 의미합니다.

그렇다면 varience란 무엇일까요?

1. Varience(변성)

변성(Varience)이란, 계층관계에서 서로 다른 타입간에 어떤관계가 있는지를 나타내는 개념입니다.
아래와 같이 생각해봅시다.

위의 컬렉션은 List이며, 타입인자(Type argument)로 E를 받고있습니다.

open class Car {
    open fun startEngine(){

    }

    open fun accelerate(){

    }
}




class `마티즈` : Car() {
    override fun startEngine() {
        super.startEngine()
    }

    override fun accelerate() {
        super.accelerate()
    }
    
    
}


// Car(부모) -> 마티즈(자식)

그리고 또 하나의 상속구조가 있습니다.
마티즈는 자동차로 치환될 수 있으며, 이는 리스코프 치환원칙을 따르고 있습니다.

그렇다면 여기서 문제인데, Car가 마티즈의 부모일때, List<마티즈>는 List<Car>로 업캐스팅이 가능할까요?

fun main (){
    val 마티즈: 마티즈 = 마티즈()
    val carList : List<Car> = mutableListOf<마티즈>(마티즈)
}

네 가능합니다.
해보시면, 에러없이 잘 담기는걸 볼수 있습니다.
타입인자(Type argument)에 있는 타입이 부모타입과 자식관계가 형성되어있을때, 기저 타입(Base Type) 또한 치환이 가능한가보네요.
그러면 저희도 한번 만들어 봅시다.

class Box<T : Car>(val t: T) {

    fun gogo() {
    }

    fun engine() {
    }

}

엇.. 근데 빨간줄이 그어져있는걸 볼수 있습니다.

리스코프 치환원칙(하위 타입(subtype)은 언제나 상위 타입(supertype)으로 대체될 수 있어야 한다)에 따르면 대체될수 있어야하는데 왜 그럴까요?

Invarient(무공변성)

일반적으로 제너릭 타입은 Invarient 타입입니다.

 fun main(){
  val anys : Cup<Any> = Cup<Int>() // Type mismatch
  
 val nothings : Cup<Nothing> = Cup<Int>() // 오류
 
 }

기존에 A가 B의 서브타입이였어도, 제너릭에서는 서로 상관이 상관이 없습니다.
그렇다면 이를 관련시키려면 어떻게 해야할까요?
만약 어떤 관련성이 필요하다면 out과 in이라는 variance 한정자를 붙이면 가능합니다.

Covarient(공변성)

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

//이는 A가 B의 서브타입일때, Cup<A>가 Cup<B>의 서브타입이라는 의미이다.

// 상위 = 하위 라고 생각하면 편하다.
class Cup<out T>
open class Dog

class Puppy : Dog()

fun main(){
 val b: Cup<Dog> = Cup<Puppy>() //ok
 val a: Cup<Puppy> = Cup<Dog>() //오류
}

contravarient(반변성)

in 한정자는 반대의미입니다.
in 한정자는 타입 파라미터를 contravariant(반변성)으로 만들게 됩니다.

//A가 B의 서브타입일때, Cup<A>가 Cup<B>의 슈퍼타입을 의미한다.

class Cup<in T>
open class Dog
class Puppy() : Dog()

fun main(args: Array<String>){
 val b: Cup<Dog> = Cup<Puppy>() // 오류
 val a: Cup<Puppy> = Cup<Dog>() // OK
}

함수 타입

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

예를 들어 (Int)-> Any 타입의 함수는 아래와 같이 작동한다.

  • (Int) ->Number
  • (Number)->Any
  • (Number)->Number
  • (Number)-> Int

코틀린 함수타입의 든 파라미터 타입은 contravariant이다. 또한 모든 리턴 타입은 covariant이다.

함수타입을 사용할때는 이처럼 자동으로 variance 한정자가 사용된다.

variance 한정자의 안정성

자바의 배열은 일반적으로 covariant(out)이다.
그런데 자바의 배열이 covariant이기 때문에 큰 문제가 발생한다.

Intger[] numbers = {1,4,2,1};
Object[] objects = numbers;

objects = numbers;

objects[2] = "B" // 런타입 오류

Integer를 Obeject로 캐스팅해도 실질적인 타입이 바뀌는것이 아니다.
이는 자바의 명백한 결함이다.

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

제너릭의 장점은?

  1. 클래스 외부에서 타입을 지정해주기 때문에 타입체크가 필요가 없습니다.

  2. 비슷한 기능을 지원하는 경우, 코드의 재사용성이 높아지게 됩니다.


    참고

    가변성(Variance) 알아보기 - 공변, 무공변, 반공변

    코틀린(Kotlin) - 런타임에서의 제네릭의 동작

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글