티스토리 뷰

 

재사용을 위한 과도한 유연성

다양한 타입에 동일한 로직을 적용하기 위해 코드 재사용을 과도하게 하려는 경우가 있습니다.

파라미터를 전부 Any로 받는 것이 대표적인 예시입니다.

open class Car
class K3 : Car()
class Avante : Car()

//“차에 대한 설명서” 함수
fun getInstruction(car: Array<Any>) {
    println("car = ${car.size}")
}

많이 극단적인 예제이기는 합니다..

open class Fruit
class Apple : Fruit()

fun main() {

    val k3 = K3("k3", "기아")
    val avante = Avante("아반떼", "현대")

		val fruits: Array<Apple> = arrayOf(Apple())
    getInstruction(fruits) // Type mismatch 에러
}

getIstruction 함수는 “차에 대한 설명서” 의도로 만든 것이지만 이를 모르고 과일인 Apple 타입을 가진 Array를 집어 넣을 수 있습니다. 인텔리제이가 워낙 똑똑해서 미리 이런 에러를 잡아 내긴 하지만 재사용을 위해 Any를 자주 사용하는 것은 타입 안정성을 저하시킬 수가 있습니다. ⇒ 제네릭을 사용하는 이유

 


제네릭은 기본적으로 불변성

자바에서는 기본적으로 제네릭은 타입 불변성을 강요했습니다. 제네릭 함수가 파라미터 타입 T 를 받는다면 T 의 부모 클래스나 자식 클래스를 사용하는 것이 불가능했습니다. 즉, 타입이 정확히 일치해야만 했습니다.

코드에 제약은 크게 걸수록 안정성은 높아진다고 생각합니다.

open class Car
class K3 : Car()
class Avante : Car()

fun getInstruction(car: Array<Car>) {
    println("car = ${car.size}")
}
fun main() {
    val cars = arrayOf(K3())

    getInstruction(cars)// Type mismatch 에러
}

제네릭은 불변성이므로 부모 클래스든, 자식 클래스든 타입이 정확히 일치하지 않으면 에러를 발생시킵니다.

fun getInstruction(car: List<Car>) {
    println("car = ${car.size}")
}

fun main() {
    val cars = listOf(K3())

    getInstruction(cars)
}

하지만 car: Array<Car> 를 car: List<Car> 로 바꿔준다면 위 코드는 문제 없이 컴파일이 됩니다.

 

제네릭은 기본적이고 불변성이고, 타입이 정확인 일치해야 받을 수 있는데 왜 Array<T>는 안되고, List<T>는 되는 걸까요?

 

Array<T> 는 가변(mutable) 이지만 List<T> 는 불변(immutable) 입니다. Array 의 아이템은 변경할 수 있지만 List 의 아이템은 변경할 수 없습니다. 변경할 수 없다는 점 때문에 안정성이 보장된다 생각하여 getInstruction(car: List<Car>)에서 받을 수 있습니다.

 

또한 Array<T> 는 class Array<T> 로 정의되어 있고, List<T> 는 interface List<out E> 로 정의되어있는데, 바로 List 제네릭 타입에 사용된 out 덕분입니다.

 


<out T> 으로 공변성(covariance) 사용하기

fun copyFromTo(from: Array<Car>, to: Array<Car>){
    for (i in from.indices){
        to[i] = from[i]
    }
}

fun main() {
    val cars1 = Array<Car>(3) { _ -> Car() }
    val cars2 = Array<Car>(3) { _ -> Car() }
    copyFromTo(cars1, cars2)
}

copyFromTo() 함수는 from 배열의 객체를 순회하면서 to 배열로 값을 넣어주는 함수입니다.

이때 전달 받은 두 배열의 크기는 동일하다고 가정하겠습니다.

 

위와 같이 구현 할 경우에는 별 문제 없이 동작합니다. 왜냐하면 copyFromTo() 함수의 파라미터로 Car 타입의 배열로 정의했고, 그에 맞춰 넘겨줬기 때문입니다.(불변성)

fun main() {
    val cars1 = Array<K3>(3) { _ -> K3() }
    val cars2 = Array<Car>(3) { _ -> Car() }
    copyFromTo(cars1, cars2) // type mismatch
}

하지만 위 코드는 에러가 발생합니다. 왜냐 코틀린은 Array<Car> 자리에 Array<K3> 를 전달하지 못하도록 막기 때문입니다.

(불변성 위배)

 

이때 out 을 사용하면 type mismatch 을 해결할 수 있습니다.(공변성 사용)

fun copyFromTo(from: Array<out Car>, to: Array<Car>){
    for (i in from.indices){
        to[i] = from[i]
    }
}

이때 주의 할점이 있습니다. 아래와 같은 코드가 그 경우입니다.

fun copyFromTo(from: Array<out Car>, to: Array<Car>) {
    for (i in from.indices) {
        from[i] = Car() 
    }

		from[0] = K3() // 얘!
}

 

Nothing을 요구

인텔리제이는 이렇게 말하고 있습니다.

 

제네릭 타입에 out 키워드를 넣어준 from은 Nothing 타입을 write 해주길 원하는데, 왜 K3 타입을 write하는가?”

 

이는 from이 <out T> 제네릭 타입을 쓰면서 공변성을 가지게 되어 생긴 문제인데,

공변성을 가지게 되면 값에 대한 read만 가능하고, write이 불가능해집니다.

 


Read만 가능하고 Write는 불가능한 이유

 

read만 가능한 이유

 

Array<out Car> 타입인 from은 부모가 Car인 것을 컴파일러가 인지하고 있습니다. 그렇다면 from의 값을 read 할 때에는 Car, K3, Avante 중 하나일 것도 알 것이며, 이는 모두를 포함하는 Car로 할당 해 줄 수 있으므로 read를 할 때에는 문제가 발생하지 않습니다.

 

write가 불가능한 이유

 

그러나 write의 경우에는 좀 다릅니다. 공변성을 사용한 from에게 실제로는 Array<K3>을 넘겨줬습니다.

그러나 메소드에서 from은 Array<out T>로 선언되어 있으므로, 실제 from이 Array<Car>인지, Array<K3>인지, Array<Avante>인지 모릅니다. from의 실제 Array Type을 모르는 메소드가 함부로 값을 write 할 수 없으므로 문제가 발생합니다.

 

즉, out 키워드를 사용하여 공변성을 사용할 경우에는 읽기만(read-only) 사용 가능하다고 볼 수 있는 것이죠.

 

*공변성 : A 타입이 B 타입의 서브 타입일 때, C<A>는 C<B>의 서브타입 입니다.

read-only 가 있다면 반대로 쓰기 전용(write-only) 도 있습니다.

 


<in T> 으로 반공변성(contravariance) 사용하기

공변성을 사용할 때 읽기만 가능하다고 해서 out을 사용하였는데, 반공변성에서는 반대로 쓰기만 가능하여 in을 사용합니다.

fun copyFromTo(from: Array<out Car>, to: Array<Car>){
    for (i in from.indices){
        to[i] = from[i]
    }
}

T 가 Car 타입이거나 Car 의 하위 클래스라면 아무 Array<T> 로부터 객체를 복사하는 것은 정상적입니다. 공변성은 코틀린이 from 파라미터를 유연하도록 하는 게 안전하다는 것을 보장해줍니다.

 

그렇다면 이번에는 to 파라미터를 살펴보겠습니다. to 파라미터의 타입은 변경 불가능한 Array<Car> 입니다.

to 파라미터에 Array<Car> 을 전달한다면 문제가 없겠지만, Car 의 부모 클래스를 전달하고 싶을 때는 어떨까요?

 

이때 in을 사용할 수 있습니다.

fun main() {
    val cars1 = Array<K3>(3) { _ -> K3() }
    val cars2 = Array<Any>(3) { _ -> Any() }
    copyFromTo(cars1, cars2)// type mismatch
}

fun copyFromTo(from: Array<out Car>, to: Array<Car>){
    for (i in from.indices){
        to[i] = from[i]
    }
}

위 코드처럼 하게 되면 제네릭의 불공변성 때문에 에러가 발생하게 됩니다.

fun copyFromTo(from: Array<out Car>, to: Array<in Car>){
    for (i in from.indices){
        to[i] = from[i]
    }
}

따라서 in 을 붙어주게 되면 이를 해결할 수 있습니다.

to 파라미터에 원래 요청된 타입이나 그 타입의 조상 타입이 가능하게 하는 권한(반공변성)을 요청한 것입니다.

하지만 아래 코드는 에러를 발생시킵니다. 위에서 말씀 드린것처럼 반공변성은 Write만 가능하기 때문입니다.

fun copyFromTo(from: Array<out Car>, to: Array<in Car>){
    for (i in from.indices){
        to[i] = from[i]
    }

    var any: Car = to[0]
}

이유는 간단합니다. 반공변성으로 인해 제네릭 타입 또는 조상 타입을 가능하게 하였으므로,

제네릭 타입으로 해당 값을 read 할 때 해당 값의 타입이 제네릭 타입의 조상 타입일 경우 문제가 발생하기 때문입니다.

fun copyFromTo(from: Array<out Car>, to: Array<in Car>){
    for (i in from.indices){
        to[i] = from[i]
    }

    from[0] = K3() as Nothing

    var any: Any = to[0] as Any
}

위 코드와 같이 강제로 캐스팅 해주면 문제는 없습니다.

결국 타입의 안정성을 보장해주기 위해 제네릭을 사용하게 되는데 out, in 을 이용하여 제네릭이 갖는 타입에 대한 유연성을 조금이나마 늘려줄 수 있습니다.

 

in,out 사용 이유 ⇒ 제네릭을 통해 타입의 안정성 제한을 조금 느슨하게 하면서 코드의 유연성을 올려주기 위해!!

'Language > Kotlin' 카테고리의 다른 글

[Kotlin] 데이터 집합 표현에 data 한정자를 사용하라  (0) 2022.06.16
댓글
댓글쓰기 폼
공지사항
Total
248,450
Today
825
Yesterday
1,065
링크
«   2022/10   »
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
글 보관함