[Kotlin] Generics 도장깨기 2편

H43RO·2021년 9월 3일
2

Kotlin 과 친해지기

목록 보기
10/18
post-thumbnail
post-custom-banner

💡 이전 포스팅과 이어집니다!

이전 포스팅에선 제너릭 함수, 제너릭 클래스 등의 용법과 제약 조건에 대해서도 배워보았다. 이번 시간엔 더 나아가 불변성 및 공변성, 반공변성의 개념에 대해서 알아보고자 한다. 용어는 생소할 수 있지만, 개념은 그리 어렵지 않으니 천천히 하나씩 살펴보도록 하자.

Invariance (불변성)

이전 포스팅에서 우리는 아래와 같은 제너릭 클래스를 만들었다.

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 (공변성) 에 대해서 알아보자.


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 (반공변성)

용어를 봐서는 잘 와닿지 않을 것이다. 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 키워드를 사용하면 된다.

profile
어려울수록 기본에 미치고 열광하라
post-custom-banner

0개의 댓글