일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- pandas
- MyVoca
- Coroutines
- boj
- Python
- ProGuard
- Coroutine
- architecture
- relay
- 쿠링
- livedata
- Kotlin
- activity
- TEST
- android
- 암호학
- 프로그래머스
- Gradle
- textfield
- MiTweet
- androidStudio
- 코루틴
- Codeforces
- 백준
- Hilt
- AWS
- Compose
- Rxjava
- 코드포스
- GitHub
- Today
- Total
이동식 저장소
[Kotlin] 제네릭과 타입 간의 관계 본문
Kotlin에서, variance라는 개념은 타입이 같으면서 타입 매개변수는 서로 다른 타입이 어떻게 연관되어 있는지 설명한다. 예를 들어 List<Int>
와 List<String>
등이 해당한다. Variance를 이해하면 타입 안정성을 해치지 않으면서 사용하기 편리한 제네릭 함수를 작성할 수 있다.
Variance?
다음과 같은 함수가 있다.
fun printContents(list: List<Any>) {
println(list.joinToString(", ")
}
list
에 List<String>
을 전달할 수 있을까? 당연히 있다. List<Int>
도 전달할 수 있다. 여기까지는 아무른 문제가 없다.
이제 다음의 함수를 보자. 이 함수는 리스트의 맨 뒤에 정수 42
를 추가한다.
fun addNumber(list: MutableList<Any>) {
list.add(42)
}
list
에 MutableList<String>
을 넘겨줄 수 있을까?
정답은 아니오. 컴파일 에러가 발생한다. 물론 String
은 Any
의 하위 타입(서브 타입)이지만, MutableList<String>
는 MutableList<Any>
의 서브 타입이 아니다. String
의 리스트에 Int
를 추가할 수는 없지 않겠는가?
첫 번째 함수는 리스트를 단순히 참조하기만 하지만, 두 번째 함수는 리스트를 수정하고 있다. 항상 뭔가를 수정하려고 할 때 문제가 발생하곤 하는데, 여기서도 마찬가지다. 따라서 함수가 수행하는 동작에 따라 읽기 전용 인터페이스를 전달할지(List
), 수정 가능한 인터페이스를 전달할지(MutableList
) 결정해야 한다.
사실 이 문제를 정확히 파악하려면 Kotlin의 제네릭 타입이 어떻게 구현되었는지 알아야 한다.
클래스, 타입, 서브 타입
갑자기 웬 타입 얘기냐고? 일단 들어 보시라.
변수의 타입은 그 변수가 참조할 수 있는 값의 범위를 정한다. 엄밀히 말해 타입과 클래스는 서로 다르다. 예를 들어 String
클래스를 활용하여 String
타입과 String?
타입의 변수를 선언할 수 있다. 즉 하나의 클래스당 적어도 두 개의 타입을 선언할 수 있다는 뜻이다.
제네릭 클래스는 어떨까? List
클래스에 서로 다른 타입 매개변수를 넘기면 다른 타입이 된다. List<Int>
, List<String>
, List<List<Int>>
등 무한히 많은 타입을 선언할 수 있다.
객체 지향을 공부해본 사람이라면 타입 간의 상하관계를 알 것이다. 타입이 B
인 변수를 언제나 타입 A
인 것처럼 사용할 수 있다면, B
는 A
의 서브 타입이다. 예를 들어 Double
은 Number
의 서브 타입이고, String
은 CharSequence
의 서브 타입이다.
서브 타입은 서브 클래스와 다르다. 예를 들어 Int
는 Int?
의 서브 타입이지만, 서브 클래스는 아니다. 서로 상속 관계가 없기 때문이다.
서브 타입과 서브 클래스 간의 차이점을 이해했다면, 맨 처음에 했던 질문을 다시 정의할 수 있다.
List<String>
을List<Any>
로 사용할 수 있는가?
이 질문을
List<String>
은List<Any>?
의 서브 타입인가?
로 바꿀 수 있다. (정답은 예다)
MutableList
를 예시로 들면, 서로 다른 임의의 타입 A
와 B
에 대해 MutableList<A>
가 MutableList<B>
의 서브 타입 또는 상위 타입이 아닐 때 MutableList
는 invariant하다고 한다. MutableList
로 만들 수 있는 여러 타입 간에 어떠한 관계도 존재하지 않는 것이다. 사실 자바에서는 모든 제네릭 클래스가 invariant하다.
반면 List
등 읽기 전용 제네릭 클래스는 B
가 A
의 서브 타입이면 List<B>
도 List<A>
의 서브 타입이다. 이것을 covariant라고 한다.
Covariant
Producer
클래스를 예시로 들어 설명하겠다. 제네릭 클래스 중 B
가 A
의 서브 타입일 때 Producer<B>
가 Producer<A>
의 서브 타입인 클래스를 covariant 클래스라고 한다. A
와 B
의 타입 관계가 그대로 적용되는 것이다.
Kotlin
에서 covariant 클래스를 정의하려면, 타입 매개변수 앞에 out
키워드를 붙여야 한다.
interface Producer<out T> {
fun produce(): T
}
T
를 covariant로 지정하면, 함수의 매개변수 또는 리턴 값의 타입 매개변수가 정확히 T
가 아니어도 된다. 예를 들어 Producer<Number>
는 Int
값을 반환할 수 있다.
Covariance vs. Upper bound
Covariance를 타입의 상계(upper bound)와 헷갈리기 쉽다. Upper bound는 T
에 들어갈 수 있는 타입을 한정짓고, covariance는 Producer<A>
와 Producer<B>
간의 타입 관계를 결정한다.
예를 들어, T
에 넣을 수 있는 타입을 제한하고 싶다면 T
의 upper bound를 정해야 한다. 그러나 Producer<Number>
를 요구하는 함수에 Producer<Int>
를 넘겨주고 싶다면 covariance를 선언해야 한다. Upper bound와 covariance를 함께 사용하는 경우도 종종 있을 것이다. 헷갈리지 말자.
보통 제네릭 클래스가 T
타입을 반환하기만 하면 covariance를 선언해도 된다(out
position 허용). 키워드로 out
을 쓰는 이유이기도 하다. 반면 covariance를 선언했다면 클래스의 함수에서 T
타입의 값을 매개변수로 받아들여서는 안 된다(in
position 금지).
out T
의 의미를 정리하자면 다음과 같다.
- 타입 관계가 전이된다.
Producer<Int>
는Producer<Number>
의 서브 타입이다. T
는out
position으로만 사용되어야 한다.
다시 처음의 List
와 MutableList
얘기로 돌아가자. List
는 T
를 out-position으로만 사용한다. 반면 MutableList
에는 T
를 매개변수로 받는 in-position 함수가 존재한다(append
등). 이런 이유 때문에 List
의 타입 매개변수는 out
을 선언되고, MutableList
는 그렇지 않은 것이다.


예외로, 생성자 매개변수와 private 함수 및 변수는 in
도 out
도 아니다. 그래서 다음 코드는 컴파일 에러가 나지 않는다.
class Herd<out T: Animal>(vararg animals: T) { ... }
물론 생성자에서 멤버 변수를 동시에 선언한다면 얘기가 달라진다.
class Herd<T: Animal>(var animals: T) { ... } // out으로 선언하면 안 된다.
그런데 타입 매개변수가 in-position으로만 사용되면 무슨 일이 생길까?
Contravariance
Contravariance는 covariance를 정확히 반대로 뒤집은 것이다. 다음 Comparator
인터페이스를 보자.
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int { ... }
}
인터페이스 내부의 함수에서 T
가 in-position으로만 쓰이고 있다. 따라서 T
를 in
으로 선언할 수 있다.
보통 타입 위계질서에서 구체적일수록 하위 타입이고 추상적일수록 상위 타입인데, contravariance를 선언하면 이게 반대로 적용된다. 다음의 그림을 보자.

Cat
의 생산자는 Animal
의 생산자로 여겨질 수 있다. 따라서 Producer<Cat>
은 Producer<Animal>
의 서브 타입이다. 반대로 Animal
의 소비자는 Cat
의 소비자로 여겨질 수 없다. Cat
의 소비자가 더 구체적인 소비자이기 때문이다. 따라서 Consumer<Cat>
이 Consumer<Animal>
의 상위 타입이다.
정리하면 다음과 같다.

in
과 out
을 같이 쓸 수도 있다.
interface MyFunction<in P, out R> {
operator fun invoke(p: P): R
}
이걸 다르게 표현하면 람다식에서 흔히 보는 (P) -> R
이 된다. P
는 in-position으로만, R
은 out-positon으로만 쓰였다.

지금까지는 variance를 클래스 또는 인터페이스의 선언부에서 일괄적으로 선언했다. 반면 클래스 전체적으로는 variance가 없지만 개별 함수 단위에서는 variance가 존재하는 경우도 있다. 예를 들어 다음의 함수를 보자.
fun <T> copyData(source: MutableList<T>,
dest: MutableList<T>) {
source.forEach { dest.add(it) }
}
source
는 읽기 전용(in-position)으로, dest
는 쓰기 전용(out-position)으로만 쓰였다. 엄밀히 따지자면 dest
는 T
의 상위 타입을 받아들여도 된다. MutableList<Int>
의 데이터를 MutableList<Number>
에 복사할 수 있지 않는가?
따라서 타입 매개변수를 다음과 같이 바꿔볼 수 있다.
fun <T: R, R> copyData(source: MutableList<T>,
dest: MutableList<R>) {
source.forEach { dest.add(it) }
}
하지만 더 짧게 쓸 수도 있다. source
가 out-position으로만 사용된다는 점에 주목하면 된다.
fun <T> copyData(source: MutableList<out T>,
dest: MutableList<T>) {
source.forEach { dest.add(it) }
}
이렇게 선언하면 source
에서 T
가 in-position으로 사용된 함수를 호출할 수 없다. 이것을 type projection이라고 말한다. 매개변수의 쓰임새를 제한하는 것이다.
물론 source
를 List<T>
로 선언하면 한방에 해결된다. Variance를 이렇게 쓸 수도 있다는 걸 보여주고 싶었다.
이제 모든 타입을 받아들일 수 있는 경우를 생각해 보자.
* (와일드 카드)
*
를 사용하면 제네릭 타입에 대한 어떠한 정보도 제공하지 않겠다는 뜻이 된다. 임의의 타입을 나타낸다고 봐도 좋다. 단, MutableList<*>
는 MutableList<Any?>
와 다르다. 앞의 것은 타입이 고정되어 있지만 그 타입을 매개변수로 나타내지 않는 것이고, 뒤의 것은 모든 값을 넣을 수 있는 리스트를 말한다.
MutableList<*>
의 타입 매개변수를 모르기 때문에 리스트에 어떠한 값도 넣을 수 없다. 대신 값은 Any?
타입으로 읽을 수 있다. Out-position 타입이라고 봐도 좋다. 특정 변수의 타입을 모른다면 Any?
로 읽을 수 있지만, 모르는 타입의 값을 만들 수 없는 것과 같은 이치이다.
타입이 중요하지 않은 상황에서 *
를 사용할 수 있다. 예를 들어 리스트의 첫 번째 값을 출력하는 printFirst
함수는 리스트의 타입에 상관없이 (존재한다면) 첫 번째 값을 출력한다.
fun printFirst(list: List<*>) {
if (list.isNotEmpty()) println(list.first())
}
사실 제네릭은 Kotlin의 여러 기능 중에서도 이해하기 까다로운 편이다. 여러 번 복습하자.
'Primary > Kotlin' 카테고리의 다른 글
[Kotlin] 클래스 안에 확장 함수? (0) | 2022.06.16 |
---|---|
[Kotlin] 함수 타입 상속받기 (0) | 2022.06.16 |
[Kotlin] 런타임에서의 제네릭과 reified (0) | 2022.06.12 |
[Kotlin] 제네릭 타입 제한하기 (0) | 2022.06.10 |
[Kotlin] &&와 and의 차이 (0) | 2022.06.03 |