(Kotlin/코틀린) 공변성(out) 완전 정리 - 왜 List 인가?
개요
- Kotlin 제네릭을 보면
interface List<out T>를 보게 됩니다 - 여기서 out은 무엇일까요?
- 처음 보면 단순 키워드처럼 보이지만, 사실은 타입 안정성을 보장하는 핵심 개념(공변성, Covariance) 입니다.
- 이 글에서는 다음을 설명합니다.
- out이 무엇인지
- 왜 List는 out인지
- 언제 out을 써야 하는지
- 실무에서 어떻게 이해하면 되는지
1. 한줄 정의
out은 “이 타입은 데이터를 내보내기만 한다(Producer)”는 의미입니다.
2. enum은 사실 객체다
open class Animal
class Dog : Animal()
val dogs: List<Dog> = listOf(Dog())
val animals: List<Animal> = dogs // 이게 가능해야 할까?
- 👉 직관적으로는 가능해 보입니다.
- Dog는 Animal의 하위 타입이니까
- Dog 리스트도 Animal 리스트처럼 쓸 수 있어야 할 것 같음
- 하지만 Java에서는 기본적으로 불가능합니다.
3. 왜 문제가 되는가?
val animals: MutableList<Animal> = mutableListOf<Dog>()
animals.add(Animal()) // ❌ 문제 발생
- Dog 리스트에 Animal을 넣어버림 -> 타입 깨짐
4. 그래서 등장한 개념: 공변성 (out)
interface List<out T>
- 이 리스트는 T를 “꺼내는 것”만 허용한다
5. out이 있으면 가능한 것
val dogs: List<Dog> = listOf(Dog())
val animals: List<Animal> = dogs // ✅ 가능
- List는 값을 “읽기만” 하기 때문
- 내부에 값을 넣는 API가 없음
- 👉 안전함
6. out의 핵심 규칙
✔ out 타입은 “반환만 가능”
interface Producer<out T> {
fun produce(): T
}
❌ out 타입은 “파라미터로 사용 불가”
interface Producer<out T> {
fun consume(value: T) // ❌ 컴파일 에러
}
- 외부에서 T를 넣게 되면 타입 안정성이 깨질 수 있음
7. Producer 관점으로 이해하기
- Producer
- out = 생산만 한다 (꺼낸다)
- in = 소비만 한다 (넣는다)
8. MutableList는 왜 out이 아닐까?
interface MutableList<T>
fun add(element: T)
- 👉 값을 “넣을 수 있음”
- 따라서:
- out을 붙이면 add()가 불가능해짐
- 그래서 invariant(무공변)