오늘도 더 나은 코드를 작성하였습니까?

스타 프로젝션 본문

Kotlin in Action/코틀린 답게 사용하기

스타 프로젝션

hik14 2021. 3. 4. 15:16

타입인자가 없을 표기하기 위해 <*>을 사용한다.

 

MutableList<*> 는 Mutable<Any?>와 같지 않다.

 

Mutable<Any?> -  모든 타입의 원소를 담을 수 있는 리스트.

MutableList<*> - 타입에 대한 정보가 정해지지 않은 리스트.

 

단, 타입이 정해지지 않았을 뿐이지 모든 타입을 담을수 있는것이 아니고 원소의 타입은 어찌 되었든 Any?(최상위)의 하위 타입이린간 분명하다.

fun main() {

    val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
    val chars = mutableListOf('a', 'b', 'c')

    val unknownElements: MutableList<*> =
        if(Random.nextBoolean()) list else chars

    //unknownElements.add(42)
    println(unknownElements)
}

여기서 unknownElements는  MutableList<out Any?> 처럼 동작한다. 

unknownElements의 원소의 타입을 모를지라도 Any? 타입으로 원소를 꺼내올 수 있지만, 반대로 add는 사용할 수 없다. 

 

타입파라미터를 시그니처에서 전혀 언급하지 않거나, 데이터를 그저 읽기만하고 타입에 관심이 없어 타입인자 정보가 중요하지 않을때 사용한다.

fun printFirst(list: List<*>){
    if (list.isNotEmpty()){
        println(list.first())
    }
}

fun main() {
    printFirst(listOf("kotlin", "java"))
}

스타프로젝션을 쓰는 쪽에서 더 간결하지만 제네릭 타입 파라미터가 결국 어떤 타입인지 굳이 알 필요가 없을 때만 스타 프로젝션을 사용할 수 있다.

 

 

스타프로젝션을 사용하는 방법과 실수를 알아보자

 

사용자 입력 검증기를 만들기.

interface FiledValidator<in T>{
    fun validator(input: T): Boolean
}

object DefaultStringValidator: FiledValidator<String>{
    override fun validator(input: String): Boolean  = input.isNotEmpty()
}

object DefaultIntValidator: FiledValidator<Int>{
    override fun validator(input: Int): Boolean = input >= 0
}

fun main() {
    
    val validators = mutableMapOf<KClass<*>, FiledValidator<*>>()
    
    validators[String::class] = DefaultStringValidator
    validators[Int::class] =  DefaultIntValidator   
}

KClass는 코틀린 클래스를 표현한다.

 

FiledValidator<*>는 모든 검증기 타입을 표현한다. 그렇기 String을 검증하는데 있어 안전하지 못하다고 컴파일러는 판단한다.

즉, 알수 없는 타입의 검증기에 구체적인(String, Int)타입의 검증기를 넘기면 안전하지 못하단 뜻이다.

 

그렇다면 타입 캐스팅을 해보자.

위와 같이 안전하지 않은 캐스팅이란 경고를 보게된다.

fun main() {

    val validators = mutableMapOf<KClass<*>, FiledValidator<*>>()

    validators[String::class] = DefaultStringValidator
    validators[Int::class] =  DefaultIntValidator

    val stringValidator = validators[Int::class] as FiledValidator<String>
    println(stringValidator.validator(""))
}

Int검증기를 가져와서 String  검증으로 타입변환시 아무런 문제가 발생하지 않는다

하지만, 검증 메소드 내부에서 validator에서 오류가 발생한다.

실행시점에 모든 제네릭 타입 정보는 사라지기 때문에 타입캐스팅 자체는 문제가 없으나 메서드안에서 객체나 프로퍼티를 사용할경우 문제가 문제가 된다. 전적으로  개발자이 책임이 되버린다.

 

 

안전하지 못한 로직은 클래스 내부에 감추어 API를 만들었다. 외부에서 잘못된 사용을 하지 않도록 하였다.

object Validators {

    private val validators =
        mutableMapOf<KClass<*>, FiledValidator<*>>()

    fun <T : Any> registerValidator(
        kClass: KClass<T>, filedValidator: FiledValidator<T>
    ) {
        validators[kClass] = filedValidator
    }

    @Suppress("UNCHECKED_CAST") // 경고무시.
    operator fun <T : Any> get(kClass: KClass<T>): FiledValidator<T> =
        validators[kClass] as? FiledValidator<T>
        ?: throw IllegalArgumentException("No validator for ${kClass.simpleName}")
}

fun main() {

    Validators.registerValidator(String::class, DefaultStringValidator)
    Validators.registerValidator(Int::class, DefaultIntValidator)

    println(Validators[String::class].validate("kotlin"))
    println(Validators[Int::class].validate(0))
}

제네릭 클래스의 타입 인자가 어떤 타입인지 정보가 없거나 타입인자가 어떤 타입인지 중요하지 않을때만 <*>을 사용해야된다.