[Kotlin] Generics 도장깨기 2편

H43RO·2021년 9월 3일

Kotlin 과 친해지기

목록 보기
10/18
post-thumbnail

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

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

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
어려울수록 기본에 미치고 열광하라

0개의 댓글