오류를 방해하는 타입 안전성(2)

·2021년 12월 9일
0
post-thumbnail

📌 제네릭: 파라미터 타입의 가변성과 제약사항


제네릭을 사용하면 다양한 타입에서 사용 가능한 코드를 만들 수 있다. 동시에 컴파일러는 제네릭 클래스 또는 함수가 의도하지 않은 타입에서 사용되는지 검증할 수 있다.

Java에서 제네릭은 타입 불변성을 강조했다. 제네릭 함수가 파라미터 타입 T를 받는다면 T의 부모 클래스나 자식 클래스를 사용하는 것이 불가능했다. 코틀린은 여기에 예외를 적용했다.

공변성 허용 : 유저가 컴파일러한테 파라미터 타입 T의 자식 클래스도 허용해달라 하는 것
반공변성 허용 : 파라미터 타입 T의 부모 클래스를 타입 T가 필요한 자리에서 쓸 수 있도록 한다는 것

타입 불변성

메소드가 클래스 T의 객체를 받을 때, T 클래스의 자식이라면 어떤 객체든 전달할 수 있다. 예를들어 Animal의 인스턴스를 전달할 수 있다면 Animal의 자식인 Dog의 인스턴스 역시 전달 가능하다.
하지만 메소드가 타입 T의 제네릭 오브젝트(List<T>...) 를 받는다면 T의 파생클래스는 전달할 수 없다.
즉, List<Animal>을 전달할 수는 있지만 List < Dog >을 전달할 수는 없다는 뜻이다.

🔥이것이 바로 타입 불변성이다


open class Fruit
class Banana : Fruit()
class Orange : Fruit()

타입 불변성에 대한 예를 들어보기 위해 Fruit 클래스와 Fruit 클래스를 상속받는 2개의 클래스를 만들었다.


fun receiveFruits(fruits: Array<Fruit>) {
    println("Number of fruits : ${fruits.size}")
}

Fruit 바구니가 Array< Fruit > 로 표현되어 있다고 가정하고 바구니를 다루는 메소드를 만든다.

receiveFruits


fun receiveFruits(fruits: Array<Fruit>) {
    println("Number of fruits : ${fruits.size}")
}

val bananas: Array<Banana> = arrayOf()
receiveFruits(bananas) 

💻 출력

Kotlin: Type mismatch: inferred type is Array<Typeinvariance.Banana> but Array<Typeinvariance.Fruit> was expected

이런 제약은 코틀린이 가진 제네릭에 대한 타입 불변성 때문에 발생한다. Banana의 바구니는 Fruite 바구니에서 상속받지 않았다. Array<Banana> 가 Array<Fruit>을 인자로 받는 메소드에 인자로 전달될 수 있다면 receiveFruits() 메소드가 Orange를 Banana로 취급을 하게 되면서 캐스팅 예외가 발생된다.
오렌지는 바나나처럼 취급될 수 없다.

코틀린은 Banana가 Fruit을 상속받았더라도 Array<Banana>를 Array<Fruit>으로 취급해서 전달하는 것을 막아서 제네릭을 타입 안전적으로 만들었다.


fun receiveFruits(fruits: List<Fruit>) {
    println("Number of fruits : ${fruits.size}")
}

val bananasList: List<Banana> = listOf()
receiveFruits(bananasList)

Array<Fruit> 에서 List<Fruit>으로 변경하니 오류없이 코드가 실행된다.
그 이유는 Array<T>는 뮤터블하지만 List<T>는 이뮤터블하다.

개발자가 Orange는 Array<Fruit>에 추가할 수 있지만 List<T>에는 추가할 수 없다. 코틀린은 두 타입을 정의하는 방식을 다르게 함으로서 이런 차이를 주었다.

Array<T> 는 class Array<T>로 정의되며 List<T>는 interface List<out T>로 정의된다.
가장 큰 차이는 out 에 달렸다.

공변성 사용하기

코틀린은 Array<Banana>가 Array<Fruit>을 받아야 하는 곳에 전달되는 것으로부터 우리를 보호해 준다. 그런데 가끔 이런 제약이 풀어달라고 요청해야 할 때가 있다.
공변성을 허용해서 제네릭 베이스 타입이 요구되는 곳에 제네릭 파생타입이 허용되길 원하는 것이다.
이럴때 타입 프로젝션이 필요하다.

copyFromTo

fun copyFromTo(from: Array<Fruit>, to: Array<Fruit>) {
    for (i in 0 until from.size) {
        to[i] == from[i]
    }
}

val fruitsBasket1 = Array<Fruit>(3) { _ -> Fruit() }
val fruitsBasket2 = Array<Fruit>(3) { _ -> Fruit() }
copyFromTo(fruitsBasket1, fruitsBasket2)

copyFromTo() 함수는 from 파라미터의 객체를 순차적으로 순회하면서 to 배열로 값을 넣어준다.
메소드는 두 개의 Array<Fruit>를 전달받아 정확한 타입으로 전달했다. 오류없이 코드가 잘 실행된다.

ERROR

val fruitsBasket = Array<Fruit>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket, fruitsBasket) 

💻 출력

Kotlin: Type mismatch: inferred type is Array<Banana> but Array<Fruit> was expected

코틀린은 Array<Fruit> 자리에 Array<Banana>를 전달하지 못하도록 막는다.
copyFromTo()은 Banana가 아닌 Fruit을 Array<Banana>에 추가할 수 있기 때문이다.

from 파라미터는 파라미터의 값만 읽기만 하기 때문에 Array<T>의 T에 Fruit 클래스나 Fruit 클래스의 하위 클래스가 전달되더라도 아무 위험이 없다. 이럴 경우 from 파라미터에 공변성을 허용한다.

Array<out Fruit>

fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) {
    for (i in 0 until from.size) {
        to[i] == from[i]
    }

}

Fruit의 자식 클래스들을 전달 가능하게 만들기 위하여 from: Array<out Fruit> 문법을 사용했다.

코틀린은 from 레퍼런스에 데이터가 새로 들어가게 하는 메소드 호출이 없다는 사실을 확인하고 호출되는 것을 확인하여 검증한다. from에 데이터를 전달하려고 하는 경우에는 오류가 발생한다

ERROR

fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) {
    for (i in 0 until from.size) {
        to[i] == from[i]
       
      from[i] = Fruit() 
      from.set(i, to[i])
      
    }

}

💻 출력

Kotlin: Type mismatch: inferred type is Fruit but Nothing was expected

from에서는 읽기만 하고 to에 값을 선택하는 경우에만 from 파라미터 위치에 Array<Fruit>, Array<Banana>, Array<Orange>을 전달할 수 있다.

Array<T> 클래스는 T타입의 객체를 읽고, 쓰는 메소드를 모두 가지고 있다. 하지만 공변성을 사용하기 위해서 코틀린 컴파일러에게 주어진 어떤 값도 추가하거나 변경하지 않겠다는 약속을 해야한다.

이런 제네릭 클래스는 사용하는 관점에서 공변성을 이용하는 걸 사용처 가변성 혹은 타입 프로젝션 이라고 부른다

제네릭 타입을 사용할 때가 아니라 선언할 때 공변성을 사용한다고 지정하는 것을 선언처 가변성이라고 부른다

선언처 가변성의 좋은 예는 List 인터페이스의 정의 에서 찾아볼 수 있다.
List<out T>로 가변성이 정의가 되어있기 때문에 List<Banana> 를 receiveFruits() 메소드로 전달할 수 있는 권한을 얻을 수 있었다.

반공변성 사용하기

copyFromTo() 메소드에서 from 파라미터를 보면 T가 Fruit 타입이거나 Fruit 하위 클래스라면 아무 Array<T>로 부터 객체를 복사하는게 적절하다.
공변성에 의해 from 파라미터가 유연해졌다. 그에 반해 to파라미터의 타입은 변경 불가능한 Array<Fruit>이다.

copyFromTo() 의 to파라미터에 Fruit의 부모클래스를 전달하게 할려면 어떻게 해야될까

❗Array<Any>를 to파라미터에 전달하면 어떻게 되는지 확인해 보자

Array<Any>


val things = Array<Any>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket,things)

💻 출력

Kotlin: Type mismatch: inferred type is Array<Any> but Array<Fruit> was expected

코틀린의 기본 타입 불변성으로 인해 실행이 안된다.

in 키워드 추가

fun copyFromTo(from: Array<out Fruit>, to: Array<in Fruit>) {
    for (i in 0 until from.size) {
        to[i] == from[i]
      
    }
}

Array<Fruit>이었던 자리를 Array<in Fruit>으로 변경했다.
in 키워드는 메소드가 파라미터에 값을 설정할 수 있게 만들고, 값을 읽을 수 없게 만든다.

Array<Any>

val things = Array<Any>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket,things)

다시 실행을 해보면 문제없이 작동된다.

공변성을 위한 선언처 가변성처럼 클래스는 <in T>로 정의 될 것이다. <in T>로 정의되면 전체적으로 파라미터 타입을 받을 수만 있고 리턴하거나 다른곳에 보낼 수 없는 반공변성으로 특정된다.

where를 사용한 파라미터 타입 제한

제네릭은 파라미터에 여러 타입을 쓸 수 있도록 유연함을 제공해주지만 때때로는 유연성은 올바른 선탣이 아닐 때가 있다.

close()

fun <T> useAndClose(input: T) {
    input.close() // ERROR : Unresolved reference: close
}

위의 메소드에서 타입 T는 close() 메소드를 서포트해야 하지만 close()가 없는 타입도 존재하기 때문에 컴파일러는 close() 메소드 호출에 실패했다.

AutoCloseable

fun <T : AutoCloseable> useAndClose(input: T) {
    input.close() // OK
}

useAndClose() 함수는 AutoCloseable 인터페이스를 구현한 T만을 파라미터 타입으로 사용하면서 close() 메소드 호출에 성공했다.

입력

val writer = java.io.StringWriter()
writer.append("hello ")
useAndClose(writer)
println(writer)

💻 출력

hello

하나의 제약조건을 넣기 위해서 파라미터 타입 뒤에 콜론을 넣은 후 제약조건을 정의하면 된다.

여러 개의 제약조건을 넣을 때는 where를 사용해야 한다.
메소드 정의 끝 부분에 where절을 쓰고 콤마(,)로 구분해서 제약조건을 나열한다.

AutoCloseable을 만족하는 것에 추가로 Appendable을 제약조건으로 더하면 append() 메소드도 사용 가능한다.

Appendable

fun <T> useAndClose(input: T)
        where T : AutoCloseable, T : Appendable {
    input.append("there")
    input.close()
}

val writer = java.io.StringWriter()
writer.append("hello ")
useAndClose(writer)
println(writer)

💻 출력

hello there

스타 프로젝션

파라미터 타입을 정의하는 스타 프로젝션 <*>은 제네릭 읽기전용 타입과 raw타입을 위한 코틀린의 기능이다.

스타 프로젝션은 타입에 대해 정확히는 알 수 없지만 타입 안전성을 유지하면서 파라미터를 전달할 때 사용된다. 스타 프로젝션은 읽는 것만 허용하고 쓰는 것은 허용하지 않는다.

Star

fun printValue(values: Array<*>) {
    for (value in values) {
        println(value)
    }
    values[0] = values[1] //ERROR : Type mismatch: inferred type is Any? but Nothing was expected
}

printValue(arrayOf(1, 2))

printValue() 함수는 Array<*>을 파라미터로 받는다. 그리고 함수 내에서 어떠한 변경도 허용되지 않는다.
여기서 사용된 스타 프로젝션 <*>은 out T와 동일하지만 더 간결하게 작성할 수 있다. 스타 프로젝션은 모든 작성을 방지하고 안전성까지 제공해준다.

📌 구체화된 타입 파라미터


코틀린도 Java와 마찬가지로 타입 이레이저의 한계를 다뤄야 한다. 실행 시간에 파라미터 타입은 사용할 수 없다.
하지만 코틀린은 reified 타입 이라고 마크되어 있고 함수가 inline으로 선언되었다면 파라미터 타입을 사용할 수 있도록 권한을 준다.

Book 클래스와 그 자식 클래스들이 있다고 가정해보자.

Book class

abstract class Book(val name: String)
class Fiction(name: String) : Book(name)
class NonFiction(name: String) : Book(name)

books

val books: List<Book> = listOf(
    Fiction("Moby Dick"), NonFiction("Learn to Code"), Fiction("LOTR"))

List<Book>에는 Fiction과 NonFiction이 섞여있다.
list안의 Fiction과 Nonfiction 중 특정 타입의 첫번째 인스턴스를 찾는 코드를 만들어보자

reified

inline fun <reified T> findFirst(books: List<Book>): T {
    val selected = books.filter { book -> book is T }
    if (selected.size == 0) {
        throw RuntimeException("Not Found")
    }
    return selected[0] as T
}

println(findFirst<NonFiction>(books).name)

파라미터 타입 T를 reified러 선언했다. 함수 안에서 T를 타입 체크와 캐스팅용으로 사용 가능하다. 함수가 inline으로 선언되어 있기 때문에 함수의 바디가 함수 호출하는 부분에서 확장되며 타입 T는 컴파일 시간에 확인되는 실제 타입으로 대체된다.

Reified 타입 파라미터는 함수에 추가적인 클래스 정보를 전달하지 않도록 만들어주고, 코드에서 캐스팅을 안전하게 하는데 도움을 주고 컴파일 시간 안전성을 확보한 채로 리턴타입을 커스터마이징할 수 있게 해준다.





🔑 정리


코틀린은 평균적인 정적 타입의 언어가 되려는것이 아니라 완전 새로운 수준의 타입 안전성을 추구한다.

nullable 래퍼런스 타입을 non-nullable 래퍼런스 타입에서 분리하면서 컴파일러는 메모리 오버헤드 없이 아주 안정적인 타입 안전성을 가지게 되었다.

스마트 캐스팅 기능은 물필요한 캐스팅을 할 필요 없게 해준다.

제네릭 함수와 클래스를 사용할 때는 파라미터 타입틀 조정하여 타입 안전성과 유연성을 제공해준다.

reified 타입 파라미터는 컴파일 시간 타입 안전성을 강화해서 코드의 오류를 제거해준다.



출처 : 다재다능 코틀린 프로그래밍

profile
개발하고싶은사람

0개의 댓글