선언 위치 변성: in과 out
공변(out) 및 반공변(in) 타입 매개변수를 올바르게 사용해 보세요.
선언 위치 변성: in과 out은(는) CoddyKit의 무료 Kotlin Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Kotlin Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Kotlin Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
변성 문제
Kotlin에서 List<String>은 List<Any>의 하위 타입입니다. List가 out T로 선언되었기 때문입니다. 변성 어노테이션이 없다면 이 관계는 성립하지 않습니다.
val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings // OK because List<out T>
// MutableList<String> is NOT a subtype of MutableList<Any>:
// val m: MutableList<Any> = mutableListOf("x") // compile error
fun main() { println(anys) }out을 사용한 공변성
out T는 클래스가 T 값을 생성하기만 하고, 즉 반환하기만 하며 절대 소비하지 않는다는 뜻입니다. 따라서 Producer<Dog>는 Producer<Animal>의 하위 타입이 됩니다.
interface Producer<out T> {
fun produce(): T
}
class DogProducer : Producer<String> {
override fun produce() = "Woof!"
}
fun sound(p: Producer<Any>) = println(p.produce())
fun main() {
val dog = DogProducer()
sound(dog) // OK: Producer<String> is a subtype of Producer<Any>
}in을 사용한 반공변성
in T는 클래스가 T 값을 소비하기만 하고, 즉 매개변수로 받기만 하며 절대 생성하지 않는다는 뜻입니다. 따라서 Consumer<Animal>은 Consumer<Dog>의 하위 타입이 됩니다.
interface Consumer<in T> {
fun consume(item: T)
}
class Printer : Consumer<Any> {
override fun consume(item: Any) = println(item)
}
fun feedDog(consumer: Consumer<String>) = consumer.consume("Dog")
fun main() {
val printer = Printer()
feedDog(printer) // OK: Consumer<Any> is a subtype of Consumer<String>
}out 제약 조건: in 위치 금지
out T를 사용하면 T가 매개변수(in) 위치에 나타날 수 없습니다. 컴파일러가 이를 강제합니다.
interface ReadOnly<out T> {
fun get(): T // OK: out position
// fun set(t: T) {} // Error: T in in-position violates out variance
}
class Box<out T>(private val value: T) : ReadOnly<T> {
override fun get() = value
}
fun main() {
val box: ReadOnly<Any> = Box<String>("hello")
println(box.get())
}in 제약 조건: out 위치 금지
in T를 사용하면 T가 반환(out) 위치에 나타날 수 없습니다.
interface WriteOnly<in T> {
fun set(t: T) // OK: in position
// fun get(): T {} // Error: T in out-position violates in variance
}
class Sink<in T> : WriteOnly<T> {
override fun set(t: T) = println("Received: $t")
}
fun main() {
val sink: WriteOnly<String> = Sink<Any>()
sink.set("hello")
}Kotlin 표준 라이브러리 예시
Comparable<in T>은 반공변적이므로 Comparable<Number>는 모든 Number 하위 타입을 비교할 수 있습니다. List<out T>는 공변적이므로 고양이 목록은 동물 목록으로 사용할 수 있습니다.
fun sortNumbers(list: List<Number>) = list.sortedWith(compareBy { it.toDouble() })
fun main() {
val ints: List<Int> = listOf(3, 1, 2)
val sorted = sortNumbers(ints) // OK: List<Int> is List<Number>
println(sorted)
}불변성: MutableList
MutableList<T>는 불변적입니다. T를 생성할 수도 있고 소비할 수도 있으므로 서로 다른 T 타입 사이에 하위 타입 관계가 성립하지 않습니다.
fun addNumber(list: MutableList<Number>) { list.add(1.5) }
fun main() {
val ints = mutableListOf<Int>(1, 2)
// addNumber(ints) // compile error: MutableList<Int> is not MutableList<Number>
val nums = mutableListOf<Number>(1, 2)
addNumber(nums) // OK
println(nums)
}대안으로 사용하는 사용 위치 변성
클래스를 변경할 수 없다면 사용 위치 변성을 사용합니다. 호출 위치에서 out을 사용해 타입을 공변적으로 투영할 수 있습니다.
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
for (item in from) to.add(item)
}
fun main() {
val src = mutableListOf("a", "b", "c")
val dest = mutableListOf<Any>()
copy(src, dest)
println(dest)
}실습: Repository 패턴
읽기 전용 Repository에는 공변성을 사용하고, 쓰기 전용 Sink에는 반공변성을 사용해 명확한 데이터 흐름을 모델링합니다.
interface Repository<out T> {
fun findAll(): List<T>
fun findById(id: Int): T?
}
interface Writer<in T> {
fun save(entity: T)
}
interface ReadWriteRepo<T> : Repository<T>, Writer<T>
// ReadWriteRepo<User> is neither sub nor super of ReadWriteRepo<Admin>변성 선택 가이드
T가 어떻게 사용되는지에 따라 변성을 선택합니다. out은 생성자, 즉 T만 반환합니다. in은 소비자, 즉 T만 받습니다. 불변은 둘 다 수행합니다(MutableList, Channel).
// Quick mental model:
// out T: source/producer — List, Flow, Sequence
// in T: sink/consumer — Comparable, Continuation
// invariant: read+write — MutableList, Channel, MutableStateFlow
fun main() {
val nums: List<Number> = listOf(1, 2, 3) // List is out
println(nums)
}out 함수를 사용하는 불변 클래스
불변 클래스에서도 특정 함수 매개변수에 사용 위치의 out을 사용할 수 있습니다.
class Stack<T>(private val items: MutableList<T> = mutableListOf()) {
fun push(item: T) = items.add(item)
fun pop(): T? = items.removeLastOrNull()
}
fun printAll(stack: Stack<out Any>) {
// can only read, not write
println(stack.pop())
}
fun main() {
val s = Stack<String>()
s.push("hello")
printAll(s)
}빠른 확인
타입 매개변수의 out T는 무엇을 의미합니까?
복습
out T(공변성)는 값을 생성할 때 하위 타입 관계를 허용합니다. in T(반공변성)는 값을 소비할 때 하위 타입 관계를 허용합니다. 읽기와 쓰기를 모두 수행하는 불변 타입은 서로 다른 T 사이에 하위 타입 관계가 없습니다.
AI 튜터와 함께 Kotlin을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 51
- 레슨
- 203
자주 묻는 질문
“선언 위치 변성: in과 out” 강의는 무료인가요?
네 — “선언 위치 변성: in과 out” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Kotlin Academy 강의 전체를 잠금 해제할 수 있습니다. Kotlin Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“선언 위치 변성: in과 out”에서 뭘 배우나요?
공변(out) 및 반공변(in) 타입 매개변수를 올바르게 사용해 보세요. 브라우저에서 직접 실행하는 실습 코드로 Kotlin Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Kotlin Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Kotlin Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“선언 위치 변성: in과 out” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Kotlin Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Kotlin Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- where를 사용한 제네릭 함수와 타입 제약
- 선언 위치 변성: in과 out
- 스타 프로젝션과 *를 사용할 때
- 타입 소거와 reified 타입 매개변수