본문 바로가기
📚 Kotlin

[Effective Kotlin] 아이템 24. 제네릭 타입과 variance 한정자를 활용하라

by GroovyArea 2023. 1. 27.

자바를 공부하고 적용하면서 추상화를 하기 위해 인터페이스를 적극 활용했었고, 강타입 언어였기에 특히 타입을 잘 활용했어야 했다.

그 과정에서 제네릭을 다시 한번 공부하게 되었고, 공변, 반공변 성 등의 특징을 제네릭이 어떻게 풀어냈는지 공부했다.

extend, super 등의 한정자를 사용하여 타입을 적절히 제어했었던 기억이 있다.

 

코틀린도 마찬가지로 제네릭의 한정자를 제공한다.

자바의 결함을 개선하면서 말이지.

 

Out 한정자 

  • 파라미터를 공변으로 만든다.
class University<out T>
open class Student
class FreshMan : Student()

fun main(args: Array<String>) {

	val b: University<Student> = University<FreshMan>() // OK
    val a: University<FreshMan> = University<Student>() // compile error

Student의 서브 타입이 FreshMan 이므로 최초 제네릭을 지정한 타입의 서브 타입만이 허용된다.

 

In 한정자

  • 파라미터를 반공변으로 만든다.
class University<in T>
open class Student
class FreshMan : Student()

fun main(args: Array<String>) {

	val b: University<Student> = University<FreshMan>() // compile error
    val a: University<FreshMan> = University<Student>() // OK

최초 지정한 제네릭 타입의 슈퍼 타입 만이 허용된다.

 

Variance 한정자의 이점

 

자바 배열의 문제점

자바의 배열은 공변이다.

그렇기에 큰 문제가 발생한다.

 

가령,

Integer[] numbers = {1,2,3,4};

Object[] objects = numbers;

objects[4] = 'c'; // Runtime Exception 발생

이를  해결하기 위해 코틀린은 Array를 불공변으로 만들었다. Array<Int>, Array<String> 처럼~

 

 

out 한정자의 쓰임새

가령 이러한 클래스 구성이 있다고 생각하자.

open class Person

class Child: Person()
class Adult: Person()

 

class Box<out T> {
	
    private var value: T? = null
    
    fun set(value: T) {
    	this.value = value
    }
    
    fun get(): T = value ?: throw RuntimeException()
}    

val adultBox = Box<Adult>()
val personBox: Box<Person> = adultBox
personBox.set(Child()) // ???? 난 Adult 만 넣고 싶은데,

val childBox = Box<Child>()
val box: Box<Any> = childBox
box.set("something else.") // 아니야 Child 전용 인데, 뜬금 없는 값이 들어가는 중.
box.set(23)

out 한정자는 이렇게 사용하는 것이 아니다.

들어가는 위치가 아니라 return 하는 위치에 적용하는 것이다.

 

이를테면,

Response 가 있다.

sealed class Response<out R, out E>
class Failure<out E>(val error: E): Response<Nothing, E>()
class Success<out R>(val value: R): Response<R, Nothing>()

두개의 제네릭이 out 한정자를 사용하였고, 실패와 성공 클래스에서 return 타입을 안정적으로 뱉을 수 있고, 오류 타입과 잠재적인 값을 지정하지 않을 수 있다.

 

in 한정자의 쓰임새

open class Car
interface Boat
class Amphibious: Car(), Boat

fun getAmphibious(): Amphibious = Amphibious()

val car: Car = getAmphibious()
val boat: Boat = getAmphibious()

in 한정자에 맞는 동작이 아닌 경우이다.

문제는 없지만.

 

in 한정자를 씌운 반공변성 타입의 파라미터를 public out 한정자 위치에 사용하면 안된다.

 

올바른 쓰임새로,

public interface Continuation<in T> {
	private val context: CoroutineContext
    private fun resumeWith(result: Result<T>)
}

이런 식으로 타입 파라미터 자리에 반공변인 in 한정자를 사용하자.

 

Variance 한정자의 위치

  • 선언 부분
    • 클래스, 인터페이스 선언에 한정자가 적용 되어 내부에 모든 영향을 준다.
    • 가장 일반적인 경우
  • 활용 부분
    • 특정 변수에만 한정자가 적용된다.
    • 특별한 변수에 적용하고 싶을 경우

 

정리

  • 타입 파라미터는 기본적으로 불공변
  • out 한정자
    • 공변하게 만든다.
    • return 되는 타입에 이용 
  • in 한정자
    • 반공변하게 만든다.
    • 허용만 되는 타입에 이용
반응형