3 분 소요

안드로이드와 코틀린을 공부 하면서 out, in 키워드를 많이 봤지만 의미를 정확히 알지 못하였던 개념에 대해서 차근차근 알아가보려고 한다 😅

제네릭(Generic)

프로그래밍 언어에서는 Int, Char, String등 기본(Primitive) 데이터 타입을 지원한다. 제네릭타입을 확실히 정하지 않고 동일한 코드를 사용할 수 있도록 지원해주는 유용한 기능이다. 이는 <T> 로 많이 친숙하게 볼 수 있는 친구다.

fun <T> generic(value: T) {
    println(value)
}

fun main() {
    generic("ppeper")
    generic("26")
}

불변성이란??

제네릭은 자바와 마찬가지로 타입 불변성 을 가진다. 타입 불변성은 제네릭 타입을 사용하는 클래스나 인터페이스 에서는 일치하는 타입만 사용할 수 있다 는 것을 말한다. 즉 해당 타입의 부모,자식의 타입은 사용이 불가하다. 그러한 이유는 예시를 통하여 알아가보자.

open class Phone
class Apple: Phone()
class Samsung : Phone()

// 상속 관계로 부모에 자식 사용이 가능하다.
val iphone: Phone = Apple()

// 오케이 나도 해볼까
// Type Mismatch -> Required: Array<Phone> / Found: Array<Apple>
// ....
val iphones: Array<Apple> = arrayOf(Apple(), Apple())
val phones: Array<Phone> = iphones

🤔분명 Phone을 상속받은 Apple 클래스인데 Type Mismatch?

위와 같이 제네릭의 타입을 가지는 클래스, 인터페이스에서 클래스의 상속관계가 형식인자의 상속관계와 같이 유지되지 않는다 즉, A -> B 일때 Class<A> -> Class<B> 를 만족하지 못한다. 이를 Invariance(불변성) 이라고 한다.

불변성(Invariance) 이 존재하는 이유는 다음과 같은 문제가 일어날 수 있기 때문이다!

fun myPhones(phones: Array<Phone>) {
    // ...???
    phones[0] = Apple()
}

fun main() {
    val galaxys: Array<Samsung> = arrayOf(Samsung())
    myPhones(galaxys)
}

Samsung폰에 대한 galaxys 변수의 Array를 모르고 myPhones[0]에 Apple()을 넣어준다면 타입이 맞지 않아 문제가 발생하게 된다.


불변성에 대한 한계점

불변성컴파일 타임 에러를 잡아주고 런타임에 에러를 내지않는 안전한 방법 이다. 그러나 이는 가끔 안전하다고 보장된 상황에서도 컴파일 에러를내 개발자를 불편하게 할 수 있다.

fun copy(from: Array<Phone>, to: Array<Phone>) {
    for (index in from.indices) {
        to[index] = from[index]
    }
}

fun main() {
    val phones: Array<Phone> = arrayOf(Phone())
    val galaxys: Array<Samsung> = arrayOf(Samsung())
    copy(galaxys, phones)
    // Type Mismatch -> Required: Array<Phone> / Found: Array<Samsung>
}

phones에 galaxys를 copy를 하는 함수는 문제가 없어보인다. 하지만 위에서 말한 불변성 으로 인하여 A -> B 일때 Class<A> -> Class<B> 를 만족하지 못하여 컴파일 에러로 판단한다.

이를 해결하기 위해서는 개발자가 A -> B 일때 Class<A> -> Class<B> 를 상속 받게 바꿔 주어야한다. 이를 공병성 이라고 하며 코틀린에서는 out 키워드를 사용한다.


📍공병성, out 키워드

fun copy(from: Array<out Phone>, to: Array<Phone>) {
    for (index in from.indices) {
        to[index] = from[index]
    }
}

fun main() {
    val phones: Array<Phone> = arrayOf(Phone())
    val galaxys: Array<Samsung> = arrayOf(Samsung())
    copy(galaxys, phones)
}

위와 같이 out 키워드를 통하여 공변성 으로 변환을 통하여 불필요한 불변성 문제를 해결할 수 있다.

❗하지만 여기서 from에 Samsung()을 Write하려고 한다면 에러가 발생한다.

fun copy(from: Array<out Phone>, to: Array<Phone>) {
    for (index in from.indices) {
        to[index] = from[index]
    }
    // Type Mismatch -> Required: Nothing / Found: Samsung
    from[0] = Samsung()
}

from[0]에서는 Nothing 즉, 아무것도 입력받기를 원하지 않는다. 이유는 불변성과 비슷하다고 할 수 있다.

[Read]

from에서는 Array<out Phone>을 통하여 컴파일러가 from의 부모가 Phone인것과 sub Type인 Apple, Samasung 중 하나인것을 인지 하고 있다. 따라서 읽을때는 문제가 발생하지 않는다.

[Write]

하지만 from에 값을 쓰려고 한다면 타입이 Phone, Apple, Samsung 중 하나라는 것만 인지하고 있을뿐 실제 타입을 모르는 Array 에서 값을 쓸 수가 없는 것이다.

그렇다면 공병성과 반대되는 Read가 불가능하고 Write만 할 수 있는 것이 있지 않을까?

Read가 가능한 out 키워드가 있다면 반대로 Write이 가능한 in 키워드가 있다. 이를 반공병성(Contravariance) 라고 한다.


to 파라미터에 대해서 Array<Phone> 에 대하여 Phone의 super Type인 부모 클래스 를 전달하고 싶다면 어떻게 해야할까?

fun copy(from: Array<out Phone>, to: Array<Phone>) {
    for (index in from.indices) {
        to[index] = from[index]
    }
}

fun main() {
    val phones: Array<Any> = arrayOf(Any())
    val galaxys: Array<Samsung> = arrayOf(Samsung())
    copy(galaxys, phones)
    // Type Mismatch -> Required: Array<Phone> / Found: Array<Any>
}

위와 같이 Array<Any> 로 선언하고 싶지만 위에서 설명한 불변성으로 이는 안된다고 하였고 미리 컴파일러는 Type Mismatch를 통하여 에러를 알려 준다(고..마워😅).

이에 대해서 명시적으로 부모 클래스를 넘겨 줄 수 있는 방법이 in 키워드이다.

📍반공변성 in!

fun copy(from: Array<out Phone>, to: Array<in Phone>) {
    for (index in from.indices) {
        to[index] = from[index]
    }
}

fun main() {
    val phones: Array<Any> = arrayOf(Any())
    val galaxys: Array<Samsung> = arrayOf(Samsung())
    copy(galaxys, phones)
}

위와 같이 in 키워드를 통하여 반공변성 으로 변환을 통하여 불필요한 불변성 문제를 해결할 수 있다.

to[index] = from[index]로 반공병성은 Write할때는 문제가 되지 않지만 반대로 Read를 하려고 하면 문제가 발생한다.

fun copy(from: Array<out Phone>, to: Array<in Phone>) {
    for (index in from.indices) {
        to[index] = from[index]
    }
    val phone: Phone = to[0]
    // Type Mismatch -> Required: Phone / Found: Any?
}

이에 대한 문제는 공병성과 반대로 컴파일러가 to 매개변수를 읽으려 할때 반공병성 으로 Phone과 그에 대한 조상 타입을 명시적으로 가능하게 하였으므로 읽을때는 실제 타입을 모르기 때문에 함부로 읽을 수 없는 것이다.

정리

공병성, 반공병성에 대해서 학습을 해보았지만 간단한 예시를 통하여 해보았기 때문에 많은 소스를 접해보고 사용해 봐야 할 것 같다.

out: 꺼내와서(out 시킨다) 읽는다. -> Write은 불가능

  • 슈퍼 클래스에 서브 클래스를 사용가능하게 해준다.

in: 넣어준다(in 시킨다) 즉, Write 할 수 있다. -> Read는 불가능

  • 서버 클래스에 슈퍼 클래스를 사용가능하게 해준다.

References

맨 위로 이동 ↑

댓글남기기