💡 이전 포스팅과 이어집니다!
이전 포스팅에선 제너릭 함수, 제너릭 클래스 등의 용법과 제약 조건에 대해서도 배워보았다. 이번 시간엔 더 나아가 불변성 및 공변성, 반공변성의 개념에 대해서 알아보고자 한다. 용어는 생소할 수 있지만, 개념은 그리 어렵지 않으니 천천히 하나씩 살펴보도록 하자.
이전 포스팅에서 우리는 아래와 같은 제너릭 클래스를 만들었다.
class Rectangle<T>(val width: T, val height: T)
where T: Number, T: Comparable<T> {
fun getArea(): T {
return (width.toDouble() * height.toDouble()) as T
}
}
일단 한 번 Rectangel 클래스를 만드는 상황을 가정해보자.
Double 이라는 자료형 (클래스) 는 아래와 같은 상속관계가 있다.
class Double : Number, Comparable<Double>
Double 의 부모 클래스는 Number
이고, Comparable
을 구현하고 있다.
그런데, Rectangle<Double>
의 부모 클래스는 Rectangle<Number>
가 아니다.
이렇듯 두 개 타입이 서로 상속관계지만, 제너릭 클래스로서의 상속 관계는 성립되지 않는 것을 Invariance (불변성) 이라고 한다. 코틀린에서 제너릭의 모든 타입은 Invariance 이다.
이제 반대 개념인 Covariance (공변성) 에 대해서 알아보자.
반대 개념이라고 한다면, 대충 감이 잡힐 것이다. Rectangle<Number>
가 Rectangle<Double>
의 부모 클래스라면, 이것을 Covariance (공변성) 이라고 한다. 그런데 코틀린에서 제너릭 타입은 모두 Invariance 라고 하지 않았는가? 따라서 아래와 같은 연산자를 제공해준다.
out
키워드요녀석은 두 개 타입이 Invariance 일 때, 이를 Covariance 하게 만들어준다. 위 예시로 따지자면 Rectangle<Number>
가 Rectangle<Double>
의 부모 클래스가 되도록 하는 것이다.
아래 예시 코드를 봤을 때 한 번 생각해보자. 잘 돌아가는 코드가 맞을까?
class Rectangle<T: Number>(val width: T, val height: T)
fun main() {
val a = Rectangle<Double>(3.2, 19.6)
val b: Rectangle<Number> = a
}
정답은 X 이다.
Rectangle<Number>
에 업 캐스팅을 시도했으나, 두 타입은 불변성이기 때문에 상속 구조가 성립하지 않아 캐스팅이 될 리가 없다. 컴파일 에러가 발생할 것이다. 하지만 이 때out
키워드를 사용해보면!
class Rectangle<out T: Number>(val width: T, val height: T)
fun main() {
val a = Rectangle<Double>(3.2, 19.6)
val b : Rectangle<Number> = a
}
정상적으로 동작하는 것을 확인할 수 있다. out
이라는 키워드가, 타입의 상속구조가 제너릭 상속구조와 같다고 명시해준 것이다. 따라서 컴파일러 단에서도 상속 관계를 인식하여 정상 실행한다. 이렇듯 공변성 제너릭 타입을 만들 땐 out
키워드를 사용하면 된다.
용어를 봐서는 잘 와닿지 않을 것이다. Contravariance 는, 우리가 위에서 다뤘던 Covariance 의 상속관계와 반대로 상속관계가 만족되는 형태를 얘기한다. 반공변성에서 '반' 이라는게, 절반 (Half) 을 의미하는 것이 아니라 '반대' 할 때 '반' 을 의미 한다.
다시 Rectangle
녀석을 들춰보면서 이해해보자. 위에서 Covariance 를 설명할 때 Rectangle<Number>
가 Rectangle<Double>
의 부모 클래스라면 공변성 타입이라고 설명했다. 그러니까 이 반대의 경우, Rectangle<Double>
이 Rectangle<Number>
의 부모 클래스라면 Contravariance 라고 한다.
아까
out
키워드를 통해 두 Invariant 타입을 Covariant 타입으로 변환해줬는데, 유추할 수 있듯 반공변성 타입으로 만들어주기 위해서는in
이라는 키워드를 사용하면 된다.
in
키워드그냥 반대라고 생각하면 된다. in
키워드는 타입의 상속구조가 제너릭에서는 반대로 성립된다는 것을 명시한다.
아래 예제와 같은 동작이 가능하게끔 한다. Rectangle<Double>
이 Rectangle<Number>
의 부모가 됐다!
class Rectangle<in T: Number>(val width: T, val height: T)
fun main() {
val a = Rectangle<Number>(3.2, 19.6)
val b : Rectangle<Double> = a
}
이번 포스팅에서는 불변성, 공변성, 반공변성 개념에 대해 알아보았다. 이전 포스팅과 함께 제너릭에 대해서 감을 잡아 놓는다면, 코틀린의 내부 구현 코드도 비교적 쉽게 이해할 수 있을 것이다.
정상적으로 동작하는 것을 확인할 수 있다. out
이라는 키워드가, 타입의 상속구조가 제너릭 상속구조와 같다고 명시해준 것이다. 따라서 컴파일러 단에서도 상속 관계를 인식하여 정상 실행한다. 이렇듯 공변성 제너릭 타입을 만들 땐 out
키워드를 사용하면 된다.