코틀린 제네릭스 Generics
?
제네릭 generic
이란 개념은, Java
개발하면서 많이 마주치게 된다.
Kotlin
도 Java
와 비슷한 제네릭스 개념을 가지고 있지만, 실체화한 타입 파라미터 와 선언 지점 변성 등의 새로운 개념이 추가되었다.
이런 개념은 제네릭 활용을 좀 더 풍부하게 만들어 주는 역할을 한다.
- 제네릭 함수 및 클래스 정의
- 실체화한 타입 파라미터
- 변성
Variance
제네릭 함수 및 클래스 정의
타입 파라미터
타입 파라미터 는 제네릭 함수 및 클래스를 정의할 때 사용할 수 있는 타입이다. 제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자 로 치환해야 한다.
구체적인 타입 인자로 바꿔주는 이유는,
“이 변수는 리스트다.”
보다는,
“이 변수는 문자열을 담는 리스트다.” 로 표현할 수 있기 때문이다.
하지만 Kotlin
에서 제네릭 함수를 호출할 때, 타입 인자를 명시해도 되지만 타입 추론 덕분에 생략도 가능하다.
제네릭 함수와 프로퍼티
제네릭 함수와 프로퍼티 선언 가능 위치
- 제네릭 클래스 & 인터페이스 내부
- 최상위 위치
- 확장 함수 or 확장 프로퍼티
제네릭 함수 구성
- 제네릭 함수를 호출할 때는, 타입 인자를 명시 또는 생략 가능하다. (타입 추론을 통해 생략 가능)
제네릭 고차 함수 호출하기
- 함수의 파라미터 타입
(T) -> Boolean
에도 제네릭 타입 적용 가능하다.
제네릭 프로퍼티
- 제네릭 프로퍼티는 확장 프로퍼티에만 적용 가능하다.
일반 프로퍼티의 정의 자체가 여러 타입의 값의 저장이 불가하기 때문에, 일반 프로퍼티에 제네릭 적용은 허용하지 않는다.
제네릭 클래스
Java
와 마찬가지로 Kotlin
에서도 클래스와 인터페이스 이름 뒤에 <>
꺾쇠 기호를 통해서 제네릭 클래스를 만들 수 있다.
List
인터페이스는T
라는 타입 파라미터를 정의한다.T
는 해당 인터페이스 안에서 일반 타입처럼 사용 가능하다.
제네릭 클래스를 확장할 때는, 타입 인자를 지정해도 되지만, 지정하지 않아도 된다.
타입 파라미터 제약
sum
함수는 타입 인자가Number
로 제약되므로,Number
형이 아닌 다른 타입인 경우 에러가 발생한다.- 제약을 지정할 땐, 상한
upper bound
타입으로 지정하거나, 상한 타입의 하위 타입으로 지정해야 한다.
Int
타입의 상한 타입은Number
이다.Number
상한 타입으로 제약된 제네릭 클래스를 확장할 경우에는Int
또는 그에 맞는 하위 타입으로 제약 가능하다.
둘 이상의 타입 파라미터 제약
널이 될 수 없는 타입 파라미터 제약
제네릭의 아무런 제약이 없는 경우, 기본 상한 타입은 Any?
이기 때문에 함수 또는 클래스내에서 사용할 경우 안전한 호출을 사용해야 한다.
널이 될 수 없는 타입 파라미터 제약을 하면 안전한 호출 없이 제네릭 타입 변수를 사용 가능하다.
실체화한 타입 파라미터
Java
에서의 제네릭스는 JVM
의 타입 소거 type erasure
를 사용해 구현된다.
타입 소거
타입 소거는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다라는 뜻으로 제네릭한 클래스와 함수의 구현이 가능하다.
타입 소거의 한계는 실행 시점에 타입 인자를 검사할 수 없다라는 점이 있다.
실체화한 타입 파라미터
타입 소거의 한계를 해결하기 위해선 inline
함수가 필요하다. inline
함수를 통해 타입 인자 실체화를 할 수 있다.
reified
: 구체화하다.
- 인라이닝 과정에서 컴파일러가 타입 인자로 쓰인 구체적인 클래스를 참조하는 바이트코드를 생성하여 삽입한다.
예시: 코틀린 표준 라이브러리 filterIsInstance
함수
해당 함수는 사용하는 본문에서는 바이트코드로 아래와 같이 변환된다.
위와 같은
inline
함수를 사용하는 경우는 성능 최적화보다는 실체화한 타입 파라미터를 사용하기 위함에 있다. 좋은 성능을 유지하기 위해서는inline
함수의 크기를 계속 관찰할 필요가 있다.
클래스 참조를 대신하는 실체화한 타입 파라미터
Java
에서 코드 리플렉션을 위해 클래스 참조인 .class
구문을 사용할 것이다. Kotlin
에서는 .class
를 대응하는 ::class.java
구문이 있다.
실체화한 타입 파라미터를 사용하여 제네릭 함수를 생성한다면 좀 더 간단하게 위 코드를 구현할 수 있다.
위 코드는 실제로 안드로이드의 Activity 함수를 간단히 만들기 위해 많이 활용한다고 한다.
실체화한 타입 파라미터의 제약
- 타입 파라미터 클래스의 인스턴스 생성 불가
- 타입 파라미터 클래스의 동반 객체 메소드 호출 불가
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 전달 불가
inline
함수가 아닌 함수의 타입 파라미터를reified
로 지정 불가
변성 Variance
변성 Variance
라는 개념은 List<String>
과 List<Any>
와 같은 기저 타입(base type
) 은 같지만, 타입 인자가 다른 여러 타입이 어떤 관계가 있는지 설명하는 개념이라고 한다.
List<Any>
파라미터 타입을 가질 수 있는 함수
List<Any>
파라미터 타입을 가질 수 없는 함수
List
에 변경이 없다면 함수에서List<Any>
타입의 파라미터를 받을 수 있다.- 반대로,
List
에 변경이 발생하는 함수에서는List<Any>
타입의 파라미터를 받을 수 없다.
위와 같은 제네릭 클래스도 변성때문에 타입 인자를 지정할 때 제약을 두고, 실행 단계에서 발생할 수 있는 타입 불일치와 같은 에러를 방지할 수 있다.
하위 타입 subtype
하위 타입이란, 어떤 타입 A
의 값이 필요한 모든 장소에 어떤 타입 B
의 값을 넣어도 문제가 없다면, 타입 B
는 타입 A
의 하위 타입이다.
Int
는Number
의 하위 타입 및 하위 클래스이다.String
은CharSequence
의 하위 타입 및 하위 클래스이다.Int
는Int?
의 하위 타입이다.
무공변성 invariant
제네릭 클래스에 서로 다른 타입 인자가 들어가지만, 제네릭 클래스의 인스턴스 타입 관계가 하위 타입 관계가 성립되지 않는 상태를 무공변성이라고 한다.
e.g. MutableList<Any>
와 MutableList<String>
의 관계
String
타입은Any
의 하위 타입이 될 수 있다.- 하지만
MutableList<String>
타입은MutableList<Any>
의 하위 타입이 될 수 없다.
공변성 covariant
공변성은 하위 타입 관계 유지함을 뜻한다.
A
가B
의 하위 타입이다.Collection<A>
가Collection<B>
의 하위 타입이다.Collection<T>
클래스는 공변성이다.
공변적 제네릭 클래스
out
키워드를 활용하여 해당 제네릭 클래스는 공변성을 가지게 된다.- 공변성을 가진 제네릭 클래스의 타입 인자와 내부 메소드의 파라미터 타입과 일치하지 않더라도 타입 인자를 사용할 수 있다.
공변성의 제한
- 타입 안정성의 보장을 위해 공변적인 타입 파라미터는 항상 반환 타입 위치 같은
out
위치에만 있어야 한다.
타입 파라미터의 in
위치와 out
위치
in
위치는 타입 파라미터를 소비consume
하는 것이다.out
위치는 타입 파라미터를 생산produce
하는 것이다.
Kotlin
의 변성 규칙
구분 | 설명 |
---|---|
생성자 | in & out 위치 |
val 프로퍼티 |
out 위치 |
var 프로퍼티 |
in & out 위치 |
반공변성 contravariance
반공변성은 하위 타입의 관계가 공변 클래스의 경우와 반대인 상태이다.
반공변적 제네릭 클래스
Comparator
인터페이스는in
위치의 타입 인자를 함수의T
타입으로 소비consume
하고 있다.
String
은Any
의 하위 타입이다.Comparator<Any>
는Comparator<String>
의 하위 타입이다.
공변성, 반공변성, 무공변성
공변성 | 반공변성 | 무공변성 |
---|---|---|
Collection |
Collection |
Collection |
타입 인자의 하위 타입 관계가 유지 | 하위 타입 관계 역전 | 하위 타입 관계 성립 안됨 |
T 는 아웃 위치에만 사용 가능 |
T 는 인 위치에만 사용 가능 |
T 를 아무 위치에 모두 사용 가능 |
사용 지점 변성 Use site Variance
지금까지 봤던 변성은 클래스 자체를 제네릭 클래스로 만들면 그 클래스를 사용하는 모든 장소에서 변성 지정자가 영향을 주게 된다. 이런 방식을 선언 지점 변성 declaration site variance
라고 한다. 그리고 클래스 내부에서 특정 함수나 프로퍼티에만 변성을 지정하는 사용 지점 변성 use site variance
가 있다.
- 특정 파라미터의 타입 파라미터에 변성 지정을 해주면, 타입 프로젝션
type projection
일어나면서 타입에 제약이 가해진다.out
변성 지정자 때문에source
파라미터는T
가 아웃 위치에서 사용하는 메소드만 호출할 수 있다.
스타 프로젝션 star projection
스타 프로젝션은 제네릭 타입 인자 정보가 없음을 표현할 때 사용한다.
*
스타 프로젝션 vs Any?
Any?
타입은 모든 타입의 원소를 담을 수 있는 타입이지만, 정확히는 스타 프로젝션과는 다르다.
- 스타 프로젝션 : 어떤 정해진 구체적인 타입의 원소를 저장하지만, 그 타입을 정확히 모른다.
Any?
: 모든 타입의 원소를 저장할 수 있다.
스타 프로젝션 사용 제안
- 스타 프로젝션은 값을 만들어내는 메소드만 호출한다.
- 그 값의 타입에는 신경을 쓰지 말아야 한다.
스타 프로젝션의 함정
아래 예제는 API
의 필드를 검증하는 검증기 구현하고자 한다.
- 스타 프로젝션을 통해 알 수 없는 타입을 받고자 했지만, 알 수 없는 타입에 구체적인 타입을 넘기면 안전하지 못하다.
- 해결 방법은
Map
에서 꺼내어 타입 비교하는 방법이 있지만,unchecked cast
경고가 나고 실수가 발생하기 쉽다. validators[String::class] as FieldValidator<String>
:Warning: unchecked cast
- 해결 방법은
캡슐화를 통한 해결 방안
registerValidator()
메소드에서 특정 클래스와 검증기의 타입이 일치한 경우에만Map
변수에 저장할 수 있다.- 스타 프로젝션 타입의 꺼낼 때는 역시 캐스팅 과정에서
unchecked cast
경고가 발생하지만, 실행하는데 문제는 없다.