3 분 소요

서론

실무에서 제레릭이 많이 쓰이는지는 잘 모르겠습니다. 직접 제네릭 클래스를 만들어 사용하는 경우는 적을지라도 이미 만들어진 제네릭 클래스는 정말 많이 사용합니다. 대표적으로 List 제네릭 클래스가 있습니다. List 제네릭 클래스를 읽을 때 제네릭과 타입 파리미터 제한, 변성과 관련한 지식이 없으면 코드를 읽을 때 불편함이 있을 수 있습니다.

또, OOP 철학을 가지고 코드를 작성하지만 제네릭 없이는 OOP 철학이 담긴 코드를 작성하기에는 한계가 있는 것 같습니다. 아래에서는 제네릭이 무엇이고 타입 파리미터 제한, 실체화에 대해서 알아보겠습니다.

제네릭

제네릭은 다양한 타입의 객체들을 다루는 메서드와 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능입니다. (자바의 정석 p.670)

타입 파라미터(Type Parameter)

제네릭을 사용할 때, 타입 파라미터를 사용해 여러 타입을 포괄할 수 있도록 해줍니다. 대표적으로 List 클래스가 있습니다.


public interface List<out E> : Collection<E> {}

타입 파라미터 E를 사용하고 우리가 List 처럼 코드를 작성하면 비로소 구체적인 List 타입으로 결정됩니다. 이처럼 타입 파라미터를 사용하면 List, 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, List는 런타임에는 같은 객체로 판단되고, 실제 어떤 타입의 원소를 갖는지 알 수 없습니다. 이 문제는 reified와 inline 함수를 이용해서 해결할 수 있습니다. reified 키워드는 무조건 inline 함수와 사용이 되는데, 이 키워드를 사용하면 타입 파리미터를 인라인 된 코드에 함께 삽입합니다. 이때문에 런타임에도 타입을 검사할 수 있게 됩니다. 아래는 표준 라이브러리의 코드입니다.

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와 제안을 빡빡하게 걸음으로써 다운 캐스팅을 하는 분기 코드를 줄일 수 있고 런타임 에러도 자연스럽게 줄일 수 있습니다.

카테고리:

업데이트: