(Kotlin/코틀린) 반공변성(in) 완전 정리 - 왜 Comparator 인가?
개요
- 이전 글에서
out키워드(공변성)를 살펴봤습니다. - 이번에는 반대 개념인
in키워드, 반공변성(Contravariance) 를 다룹니다. in은out과 정반대로 동작합니다.out→ 타입을 꺼내기만 (Producer)in→ 타입을 집어넣기만 (Consumer)
- 이 글에서는 다음을 설명합니다.
in이 무엇인지- 왜
Comparator는in T인지 - 언제
in을 써야 하는지 - 실무에서 어떻게 이해하면 되는지
1. 한줄 정의
in은 “이 타입은 데이터를 받아들이기만 한다(Consumer)”는 의미입니다.
2. 공변성(out) 복습
open class Animal
class Dog : Animal()
val dogs: List<Dog> = listOf(Dog())
val animals: List<Animal> = dogs // ✅ out 덕분에 가능
List<out T>→ Dog는 Animal의 하위 타입이므로,List<Dog>도List<Animal>자리에 올 수 있음
3. 반공변성(in)이란?
in은 공변성과 반대 방향으로 타입 관계를 뒤집습니다.Consumer<in T>에서 T가Animal이라면,Consumer<Dog>자리에Consumer<Animal>을 넣을 수 있습니다.
interface Consumer<in T> {
fun consume(value: T)
}
val animalConsumer: Consumer<Animal> = object : Consumer<Animal> {
override fun consume(value: Animal) {
println("동물: ${value}")
}
}
val dogConsumer: Consumer<Dog> = animalConsumer // ✅ 가능
- Dog는 Animal의 하위 타입이지만
Consumer<Animal>을Consumer<Dog>자리에 사용할 수 있음- 👉 Dog도 Animal이니까, Animal을 소비하는 코드는 Dog도 소비할 수 있기 때문
4. 왜 이게 안전한가?
dogConsumer.consume(Dog()) // ✅ Dog을 전달 -> Animal을 받는 코드가 처리 가능
Consumer<Animal>은 Animal을 받아서 처리합니다.- Dog는 Animal의 하위 타입이므로, Dog를 넘겨도 문제 없습니다.
- 반대로,
Consumer<Dog>를Consumer<Animal>자리에 쓰면?
val dogOnlyConsumer: Consumer<Dog> = object : Consumer<Dog> {
override fun consume(value: Dog) { /* Dog 전용 처리 */ }
}
val animalConsumer2: Consumer<Animal> = dogOnlyConsumer // ❌ 위험
animalConsumer2.consume(Animal()) // Animal을 Dog 전용 코드에 전달 -> 타입 깨짐
- 👉 Dog 전용 Consumer에 Animal을 넣으면 런타임 오류 가능성
5. in의 핵심 규칙
✔ in 타입은 “파라미터로 사용 가능”
interface Consumer<in T> {
fun consume(value: T) // ✅ T를 받을 수 있음
}
❌ in 타입은 “반환값으로 사용 불가”
interface Consumer<in T> {
fun produce(): T // ❌ 컴파일 에러
}
- T를 반환하게 되면, 호출자가 기대하는 타입보다 더 상위 타입이 나올 수 있어 타입 안정성이 깨집니다.
6. 실제 사례: Comparator
Kotlin 표준 라이브러리의 Comparator를 보면:
public interface Comparator<in T> {
public fun compare(a: T, b: T): Int
}
val animalComparator: Comparator<Animal> = Comparator { a, b ->
a.name.compareTo(b.name)
}
val dogs: List<Dog> = listOf(Dog("Bori"), Dog("Navi"))
dogs.sortedWith(animalComparator) // ✅ 가능
Comparator<Animal>은 Animal을 비교합니다.- Dog는 Animal이므로, Dog 목록을 정렬할 때
Comparator<Animal>을 그대로 사용할 수 있습니다. - 👉
in T덕분에 가능한 일
7. out vs in 정리
| 키워드 | 역할 | 방향 | 허용 위치 | 예시 |
|---|---|---|---|---|
out |
Producer (생산) | 공변 (하위 → 상위) | 반환값만 가능 | List<out T> |
in |
Consumer (소비) | 반공변 (상위 → 하위) | 파라미터만 가능 | Comparator<in T> |
8. 타입 관계 방향 비교
// 공변성(out): Dog <: Animal 이면 List<Dog> <: List<Animal>
List<Dog> ──────────────▶ List<Animal> (같은 방향)
// 반공변성(in): Dog <: Animal 이면 Consumer<Animal> <: Consumer<Dog>
Consumer<Animal> ────────▶ Consumer<Dog> (반대 방향)
9. use-site variance (사용 지점 변성)
클래스 선언이 아닌, 사용하는 지점에서도 in을 붙일 수 있습니다.
fun copy(src: Array<out Animal>, dest: Array<in Animal>) {
for (i in src.indices) {
dest[i] = src[i]
}
}
src: Array<out Animal>→ 읽기만 (공변)dest: Array<in Animal>→ 쓰기만 (반공변)
10. 실무 요약
- 타입을 받아서 처리하는 역할이라면 →
in - 타입을 꺼내서 제공하는 역할이라면 →
out - 둘 다 해야 한다면 → 변성 없이 그냥
T(무공변)