[Kotlin/Java] 제네릭과 reified 키워드 파해치기 (with inline)

minH_·2025년 1월 19일

제네릭

제네릭 타입은 자바 1.5에서 처음 도입된 기능이며, 타입을 파라미터로 받는 함수, 클래스, 인터페이스를 말한다.

우리가 흔히 사용하고 있는 ArrayList를 잠깐 보자.

public class ArrayList<E> extends AbstractList<E>

우리는 제네릭 클래스를 이용하여

List<String> list = new ArrayList<>();

위와같은 형식으로 타입 파라미터를 정의하며 클래스를 생성할 수 있다.

JVM의 제네릭은 타입 소거를 사용해 구현된다. 타입 소거란 런타임 시점에 제네릭 클래스의 인스턴스에는 타입 파라미터의 정보가 사라진다는 말이다.

그럼 코틀린에서 타입 소거는 어떻게 동작하는지 살펴보자.

val nums: List<Int> = listOf(1, 2)
val strings: List<String> = listOf("a", "b")

컴파일러는 이 두 인스턴스를 서로 다른 타입으로 인식하지만, 런타임 시점에는 두 인스턴스는 완전히 타입이 같다. 위와 같은 이유로 우리는 코드를 작성할 때 List 에는 문자열만 들어있고 List 에는 정수만 들어있다고 가정할 수 있다.

fun <T> isSameType(type: Any): Boolean {
    return type is T
}

해당 함수를 보자. 위에 설명한 내용처럼 T 타입은 런타임에 소거되기 때문에, 런타임에 T가 어떤 타입인지 알 수 없다. 그래서 해당 함수는 아래와 같은 오류가 발생한다.

내용을 자세히 보면 타입 파라미터를 reified로 만들고, inline 함수로 선언하라고 한다. 그러면 reified와 inline은 무엇일까?

inline

Kotlin에선 inline 키워드를 제공하여 런타임 때 특정 키워드를 구분 가능하게 한다.
inline은 타입 파라미터로 쓰인 구체적인 클래스를 참조하는 바이트코드를 생성해 삽입 가능하기 때문에 타입 파라미터 실체화 가능. (이 내용에서 왜 inline인지 유추해볼 수 있다.)

타입 파라미터를 reified 키워드를 앞에 붙여주면 기존의 ::class.java처럼 구체적인 클래스를 참조하지 않아도 된다.

  inline fun <reified T> isSameType(type: Any): Boolean {
    return type is T
}

위에서 오류가 발생했던 함수를 inline과 reified를 통해 정상 작동하는 함수로 수정했다.

inline 함수와 reified 키워드의 조합을 다시 생각해보자.
inline 함수는 제네릭 타입 T의 구체적인 클래스 정보를 함수 내부에 바이트코드로 삽입하기 때문에, reified 키워드를 함께 사용하면 런타임에서도 해당 클래스의 타입을 알 수 있게 된다.

reified

reified 키워드는 Kotlin의 inline 함수에서만 사용 가능한 특수 키워드로, 제네릭 타입이 런타임에도 구체적으로 유지되도록 해준다. 일반적인 제네릭 함수는 타입 정보가 컴파일 시점에만 유지되고 런타임에는 지워지기 때문에, 직접적으로 타입 정보를 참조하거나 활용하기 어렵다(이를 타입 소거(Type Erasure)라고 한다). 그러나 reified 키워드를 사용하면, 런타임에서도 제네릭 타입 T의 정보를 유지하며 이를 활용할 수 있다.

예를 들어, reified를 사용하면 다음과 같은 작업이 가능해진다:

타입 확인 (is, !is 연산자):
런타임에 제네릭 타입을 활용해 객체가 특정 타입인지 확인할 수 있다.

inline fun <reified T> isTypeOf(value: Any): Boolean {
    return value is T
}
println(isTypeOf<String>("Hello")) // true
println(isTypeOf<Int>("Hello"))   // false

클래스 정보 활용 (T::class 또는 T::class.java):
제네릭 타입의 KClass나 Java Class를 참조할 수 있다.

inline fun <reified T> printClassName() {
    println(T::class.simpleName)
}
printClassName<String>() // "String"

타입 기반 객체 생성:
특정 타입의 객체를 동적으로 생성할 수 있다.

inline fun <reified T : Any> createInstance(): T {
    return T::class.java.getDeclaredConstructor().newInstance()
}

inline과 reified의 조합은 특히 Reflection이 필요했던 작업을 더 간단하고 안전하게 처리할 수 있도록 도와준다. 이는 성능 최적화와 코드 가독성 측면에서도 큰 이점이 있다.

또한 inline 키워드는 고차 함수를 파라미터로 받는 함수에서 사용할 때 유용하다.
일반적으로 고차 함수를 호출하면 함수 객체를 생성하고, 람다를 호출하는 오버헤드가 발생한다.
하지만 inline 키워드를 사용하면 함수 본문을 호출하는 곳으로 직접 삽입하여 이런 오버헤드를 제거할 수 있다.

하지만 inline으로 처리되는 코드가 커지면 바이너리 크기가 증가할 수 있으므로 필요할 때만 적절히 사용하는 것이 중요하다.

0개의 댓글