안드로이드 코틀린 언어 학습 노트 목차

제네릭·변성 — in/out·스타 프로젝션·타입 소거

제네릭은 타입을 매개변수화같은 코드를 여러 타입에 안전하게 재사용한다. 코틀린 제네릭의 까다롭지만 중요한 부분이 변성(variance)List<String>List<Any>로 쓸 수 있나? 같은 질문이다. 코틀린은 자바의 *물음표 와일드카드(? extends/? super)*를 out/in 키워드로 선언 지점에서 깔끔하게 표현한다. (출처: Kotlin Docs — Generics / Kotlin in Action 2e Ch 11.)

제네릭 기초 — 타입 매개변수와 제약

class Box<T>(val value: T)                    // 제네릭 클래스
fun <T> firstOf(list: List<T>): T = list[0]   // 제네릭 함수

// 상한 제약(upper bound) — T는 Comparable이어야
fun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b

변성 — 왜 List<String>은 List<Any>가 아닌가

기본적으로 제네릭은 무공변(invariant)MutableList<String>MutableList<Any>아니다. 왜? 만약 허용하면:

val strs: MutableList<String> = mutableListOf("a")
val anys: MutableList<Any> = strs        // 만약 허용된다면…
anys.add(42)                              // Any 리스트니 Int 추가 — strs가 깨짐!
val s: String = strs[0]                   // 💥 42를 String으로?

가변 컬렉션은 읽기·쓰기를 둘 다 하므로 위험하다. 그래서 언제 안전한지를 **out/in**으로 표시한다.

out — 생산자(공변, covariant)

타입 T생산(반환)만 하고 소비(입력)하지 않으면 out T로 표시한다 — 그러면 Producer<String>Producer<Any>로 쓸 수 있다(공변). 읽기 전용 List가 그래서 List<out E>다.

interface Producer<out T> {       // T를 반환만
    fun produce(): T
}
val p: Producer<String> = ...
val pa: Producer<Any> = p         // ✅ 안전 — String을 꺼내 Any로 보는 건 OK

// 그래서 읽기 전용 List는 공변
val strs: List<String> = listOf("a")
val anys: List<Any> = strs        // ✅ List는 List<out E> → 안전

in — 소비자(반공변, contravariant)

타입 T소비(입력)만 하고 생산하지 않으면 in T로 표시한다 — Consumer<Any>Consumer<String>으로 쓸 수 있다(반공변). Comparable<in T>가 예다.

interface Consumer<in T> {        // T를 받기만
    fun consume(item: T)
}
val ca: Consumer<Any> = ...
val cs: Consumer<String> = ca     // ✅ 안전 — Any를 받는 곳에 String 주는 건 OK
다이어그램 로딩 중…

이게 자바의 PECS(Producer-Extends, Consumer-Super)와 같은 원리인데 — 자바는 사용 지점? extends/? super를 매번 쓰고, 코틀린은 선언 지점out/in한 번 쓴다(필요하면 사용 지점 변성도 가능: Array<out T>).

스타 프로젝션 — 타입을 모를 때

타입 인자를 모르거나 상관없을 때 *(스타 프로젝션)를 쓴다 — "어떤 타입인지 모르지만 타입 안전하게". List<*>원소를 Any?로 읽을 수 있지만 쓸 수는 없다.

fun printSize(list: List<*>) = println(list.size)   // 타입 무관, 크기만

타입 소거와 reified

JVM은 런타임에 제네릭 타입을 지운다(타입 소거)List<String>List<Int>가 런타임엔 똑같은 List다. 그래서 value is List<String>불가(is List<*>만 가능). 이 한계를 *inline + reified*로 우회한다([04 함수] 참고) — 타입 인자가 인라인 시점에 박혀 런타임에 쓸 수 있다.

inline fun <reified T> List<*>.filterIsInstance(): List<T> =
    filter { it is T }.map { it as T }       // reified 라서 it is T 가능

정리 — 안전한 곳에만 대입을 허용한다

변성의 핵심 — out(생산자·공변): Foo<하위>Foo<상위>, in(소비자·반공변): Foo<상위>Foo<하위>, 읽고 쓰면 무공변. 읽기 전용 List<out E>가 공변이라 상위 타입 리스트로 안전하게 쓰이고, 가변 컬렉션은 무공변이라 막힌다. 런타임 타입이 필요하면 타입 소거reified로 우회한다.

한 줄 요약 — 기본은 무공변. out T=반환만(공변, Producer), in T=입력만(반공변, Consumer). PECS를 선언 지점에서 한 번. 모르는 타입은 *(스타, 읽기는 Any?·쓰기 불가). 런타임 타입은 타입 소거되니 inline+reified로.

(출처: Kotlin Docs — Generics / Variance / Kotlin in Action 2e Ch 11 / Effective Kotlin Item 24.)

Flow·StateFlow·SharedFlow — 비동기 스트림DSL·연산자 오버로딩·중위 함수