Kotlin - Generic
서론
실무에서 제레릭이 많이 쓰이는지는 잘 모르겠습니다. 직접 제네릭 클래스를 만들어 사용하는 경우는 적을지라도 이미 만들어진 제네릭 클래스는 정말 많이 사용합니다. 대표적으로 List 제네릭 클래스가 있습니다. List 제네릭 클래스를 읽을 때 제네릭과 타입 파리미터 제한, 변성과 관련한 지식이 없으면 코드를 읽을 때 불편함이 있을 수 있습니다.
또, OOP 철학을 가지고 코드를 작성하지만 제네릭 없이는 OOP 철학이 담긴 코드를 작성하기에는 한계가 있는 것 같습니다. 아래에서는 제네릭이 무엇이고 타입 파리미터 제한, 실체화에 대해서 알아보겠습니다.
제네릭
제네릭은 다양한 타입의 객체들을 다루는 메서드와 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능입니다. (자바의 정석 p.670)
타입 파라미터(Type Parameter)
제네릭을 사용할 때, 타입 파라미터를 사용해 여러 타입을 포괄할 수 있도록 해줍니다. 대표적으로 List 클래스가 있습니다.
public interface List<out E> : Collection<E> {}
타입 파라미터 E를 사용하고 우리가 List
하지만 코드 재사용성은 항상 이론적으로는 맞는 얘기지만 항상 물음표가 따라오기는 합니다. 다른 얘기이긴 하지만 함수를 작성할 때 하나의 일관된 프로세스에서 생각의 단위를 쪼갠다는 것은 전적으로 동의하지만 코드 재사용은 살짝 동의하기 어렵습니다. KEEPER 프로젝트만 보더라도 재사용성을 한 메서드는 Util 클래스의 메서드밖에 없었던 것 같습니다. 이런 얘기를 하는 이유가 바로 밑에서 나오는 타입 제한 때문입니다. 제네릭 정의에서 느끼셨을지도 모르지만 재사용성 얘기는 나오지 않고, 타입 체크에 관한 얘기가 주로 이루고 있습니다.
타입 파리미터 제약(Type parameter Contraint)
타입 파리미터 제약은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능입니다. (코틀린 인 액션 p.390)
이때 타입에 관한 제약과 인터페이스 제약이 있습니다. 단순히 upper bound 제약을 걸 수도 있지만 특정 인터페이스를 꼭 구현하도록 제약을 걸 수 있습니다. 아래의 예시를 봅시다.
fun <T : Number> List<T>.sum() : T {} // upper bound
fun <T> ensureTrailingPeriod(seq: T) where T : CharSequence, T : Appendable {} // 인터페이스 구현
위처럼 Number의 하위 타입만 올 수 있도록 제한하고, 인터페이스를 구현하도록 제한을 걸 수 있습니다.
제네릭을 공부하면서 느낀점이 범용적인 클래스에서는 딱히 upper bound를 두지 않습니다. 하지만 직접 제네릭 클래스를 만들 때는 최대한 upper bound로 타입을 꽉 쪼으고 시작합니다. 이유는 이렇게 해야 다른 개발자분들이 내 코드를 쓸 때 런타임에 에러가 나지 않을 것이고, 안정성 있는 코드가 되기 때문이지 않을까 생각을 했습니다. 이는 나중에 다운 캐스팅에 관한 얘기를 할 때 다시 꺼내도록 하겠습니다.
실체화
실체화는 제네릭 타입의 실제 타입 정보를 런타임에 사용할 수 있도록 하는 개념입니다. 자바와 코틀린은 기본적으로 컴파일 후 타입 소거(Type Erasure)을 겪는데 아래의 코드는 컴파일 후의 결과값 입니다.
// 컴파일 전
class Box <T> (
private var value : T
) {
fun set(obj : T) : Unit {
this.value = obj
}
fun get() : T {
return this.value
}
}
// 컴파일 후
public final class Box {
private Object value;
public Box(Object value) {
this.value = value;
}
public final void set(Object obj) {
this.value = obj;
}
public final Object get() {
return this.value;
}
}
List
public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
for (element in this) if (element is R) destination.add(element)
return destination
}
inline 함수를 사용하는 이유는 고차함수를 호출할 때 새로운 함수 객체를 생성하지 않고 객체 생성 비용을 줄이고자 함이고 또 다른 것은 실체화를 통한 런타임 타입 체크를 하기 위함입니다.
결론
제네릭 클래스를 실무에서 많이 작성할지는 모르겠습니다. 하지만 이미 만들어진 제네릭 클래스는 정말 많이 사용하고 있습니다. 대표적으로 List 클래스가 있는데, 컬렉션 클래스 한에서는 코드 재사용에 동의하지만, 우리가 직접 작성하는 제네릭 클래스는 재사용보다 타입 제안으로 인한 타입 체크 이점이 더 많은 것 같습니다. upper bound와 제안을 빡빡하게 걸음으로써 다운 캐스팅을 하는 분기 코드를 줄일 수 있고 런타임 에러도 자연스럽게 줄일 수 있습니다.