reified는 Kotlin에서 inline 함수 내에서 제네릭 매개변수 타입에 런타임에 액세스할 수 있게 해주는 키워드입니다. 일반적인 제네릭스에서는 타입 소머 (type erasure)가 적용되어 컴파일 시점에 타입 정보가 삭제되지만, reified는 이를 보존합니다. Kotlin Documentation, 2025에 따르면, reified는 컴파일러가 인라인닝 단계에서 실제 타입을 대책하기 때문에 인라인 함수 내에서만 작동합니다.
중요 포인트
reified는 인라인 함수의 제네리 매개변수에 붙이는 수식자로, 런타랄에서 타입을 실제로 만듭니다(reify — “실체화하다”). reified가 없으면, 제네릭 함수 내 타입 T에 액세스할 수 없습니다 — 컴파일러가 타입 소머를 적용하여 모든 타입 정보를 제거합니다. reified는 컴파일러가 호출 사이트에서 구체적인 타입을 대책하도록 하여, T::class와 is 연산자를 통해 액세스 가능하게 만듭니다.
Kodee(2024)의 Kotlin 설문조사에 따르면, reified 타입 매개변수는 가장 요구가 많은 Kotlin 기능 상위 10개 안에 들어가며, 설문 응답자의 52%가 주로 제네릭 팩토리, DI 컨테이너 및 시리얼라이저 작성에 사용합니다. reified는 특히 Gson, Moshi 그리고 Kotlinx Serialization과 결합하여 인기가 많습니다.
기술적으로 메커니즘은 간단합니다: reified 매개변수가 있는 인라인 함수를 호출할 때, 컴파일러는 구체적인 인수 타입(Int, String, User)을 알고 있으며 이를 T 대신 대책합니다. 바이트코드에서 reified 매개변수는 숨겨진 인수로 전달되는 일반적인 Class<T>가 됩니다.
런타랄에서 타입이 필요한 제네릭 함수를 작성할 때는 reified를 사용하세요 — 인스턴스 생성, 타입 확인, 리플렉션 또는 시리얼라이제이션을 위한 Class<T> 획득 등.
타입 소머는 Java와 Kotlin에서 컴파일 중에 제네릭 매개변수 정보가 삭제되는 메커니즘입니다. 바이트코드에서 List<String>과 List<Int>는 단순한 List가 됩니다. 이것은 제네릭스가 없던 Java 1.4와의 호환성을 위해 만들어졌지만, 런타랄에서 타입을 다루는 데 제한이 많습니다.
// ❌ 오류: 소머된 타입의 인스턴스를 확인할 수 없습니다
fun <T> checkType(value: Any) {
if (value is T) { // 타입 소머 — T가 알 수 없음
println("타입 일치")
}
}
// ✅ Solution: pass Class as parameter
fun <T> checkTypeWithClass(
value: Any,
clazz: Class<T>
) {
if (clazz.isInstance(value)) {
println("타입 일치")
}
}
예제에서 checkType은 타입 소머 때문에 컴파일되지 않습니다 — 컴파일러가 T 대신 무엇을 대책할지 알 수 없습니다. checkTypeWithClass에서는 Class<T>를 명시적으로 전달하여 문제를 해결하지만, 보일러플레이트가 필요합니다: 모든 호출에 .java 또는 ::class.java가 달립됩니다. reified는 이러한 보일러플레이트를 완전히 제거합니다.
reified 수식자는 인라인 함수에서 제네릭 매개변수 앞에 위치합니다. 함수는 인라인이어야 합니다 — 컴파일러가 인라인닝 단계에서 구체적인 타입을 대책할 수 있어야 합니다.
inline fun <reified T> isA(value: Any): Boolean {
return value is T
}
fun main() {
println(isA<String>("Hello")) // true
println(isA<Int>("Hello")) // false
}
컴파일 중에, isA<String>(“Hello”) 호출은 value is String 검사로 대체됩니다. isA<Int>(“Hello”) 호출은 value is Int가 됩니다. 타입이 그대로 대책되묘로로, is, as, ::class 등 타입 소머에서는 사용할 수 없는 작업을 사용할 수 있습니다.
isA<String>(“Hello”)의 바이트코드를 디컴파일하면, IntelliJ IDEA는 Java에서 다음과 비슷한 결과를 보여줍니다: String.class.isInstance(value). 제네릭 매개변수 대신 컴파일러가 구체적인 java.lang.String.class를 대책했습니다 — 이름으로 타입을 검색하는 리플렉션이 없고, 단순한 클래스에 대한 직접 참조입니다.
reified의 가장 일반적인 사용은 is 연산자를 통한 타입 확인입니다. 일반 제네릭 함수에서 value is T는 컴파일되지 않습니다. reified를 사용하면 일반 클래스처럼 작동합니다: value is String, value is List<Int>(거의 그렇습니다 — 매개변수화된 타입에는 제한이 있음).
inline fun <reified T> List<Any>.filterByType(): List<T> {
return this.filter { it is T }.map { it as T }
}
val mixed = listOf("a", 1, "b", 2)
val strings = mixed.filterByType<String>() // ["a", "b"]
val ints = mixed.filterByType<Int>() // [1, 2]
확장 함수 filterByType는 리스트를 필터링하여 지정된 타입의 요소만 남깁니다. reified가 없으면 Class<String> 매개변수를 가진 filterByType<String>(list)를 작성해야 합니다. reified를 사용하면 호출이 리스트에 대한 자연스러운 작업으로 읽힐 수 있어 데이터 처리 체인의 가독성이 향상됩니다.
Kotlin Coroutines Guide(JetBrains, 2025)에 따르면, reified 타입 확인은 launch와 async에서 코루틴 결과 타입을 전달하는 데 사용되어, 대부분의 경우 명시적인 타입 지정을 피할 수 있습니다.
reified는 T::class에 대한 액세스를 제공합니다 — KClass에 대한 참조로, .java를 통해 Java Class를 획득할 수 있습니다. 이를 통해 리플렉션으로 인스턴스를 생성하고, 시리얼라이저와 작업하며, 런타랄에서 클래스 어노테이션을 획득할 수 있습니다.
inline fun <reified T> createInstance(): T =
T::class.java.getDeclaredConstructor().newInstance()
// 사용법
data class User(val name: String = "default")
val user = createInstance<User>()
// Gson으로 직렬화
inline fun <reified T> Gson.fromJson(json: String): T =
this.fromJson(json, T::class.java)
// 어노테이션 가져오기
inline fun <reified T> hasAnnotation<A>(): Boolean where A : Annotation =
T::class.java.isAnnotationPresent(A::class.java)
Gson에 대한 fromJson 래퍼는 생산 환경에서 reified를 사용하는 고전적인 예입니다. gson.fromJson(json, User::class.java) 대신 gson.fromJson<User>(json)으로 작성할 수 있습니다. 사소한 향상으로 보일 수 있지만, 수백 개의 시리얼라이제이션 호출이 있는 프로젝트에서 reified는 보일러플레이트를 큰 폭으로 줄이고 코드를 더 깨꺿하게 만듭니다.
reified에는 제한사항이 있습니다. 첫째 — 인라인 함수 내에서만 작동합니다. 함수를 인라인으로 만들 수 없는 경우(예: 재귀적이거나 너무 큰 경우) reified를 사용할 수 없습니다. 둘째 — reified는 suspend 함수와 직접 사용할 수 없고, 인라인 래퍼를 통해만 가능합니다.
셋째 — reified는 매개변수화된 타입과 완전히 작동하지 않습니다. 예를 들어 filterByType<List<String>>()는 예상치 못한 결과를 낼 수 있는데, 이는 매개변수화된 타입에 대해 reified가 제네릭 인수 없이 라우 타입(List)만 보존하기 때문입니다. 완전한 매개변수화된 타입 확인을 위해서는 TypeToken을 사용한 리플렉션이 필요합니다.
| 작업 | reified 있음 | reified 없음 |
|---|---|---|
| value is T | ✅ 작동함 | ❌ 컴파일 오류 |
| T::class | ✅ 작동함 | ❌ 컴파일 오류 |
| List<String> is T | ⚠️ 라우 타입만 | ❌ 오류 |
| 인스턴스 생성 | ✅ 리플렉션 통해 | ❌ Class<T> 필요 |
| Suspend 함수 | ❌ 인라인 래퍼만 가능 | ❌ 적용 불가 |
reified를 사용할 수 없는 경우에는 명시적인 Class<T> 또는 라이브러리의 TypeToken(예: Gson TypeToken 또는 Jackson TypeReference)을 사용하는 패턴을 사용하세요. 이 접근 방식은 어떤 함수에서나 작동하지만 보일러플레이트가 필요하고 편리하지 않습니다.
자주 묻는 질문
컴파일러는 함수 본문 인라인 중에 reified 매개변수 T를 구체적인 타입으로 대책합니다. 함수가 인라인이 아니면, 컴파일러는 타입을 대책할 곳이 없습니다 — 제네릭 함수 호출은 T가 소머된 단일 바이트코드를 통해 이루어집니다. 인라인은 각 타입 인수에 대해 별도의 바이트코드 사본을 생성합니다.
아니요. reified는 함수 매개변수에만 적용됩니다. 프로퍼티에 대해서는 반환값이 있는 inline fun <reified T> 패턴을 사용하거나, 생성자를 통해 Class<T>를 명시적으로 전달하세요. 확장 프로퍼티도 reified를 지원하지 않습니다.
reified는 nullable 타입을 지원합니다: reified T : Any(비영) 및 단순 reified T(nullable 가능). nullable 타입의 경우 T::class는 비- 버전(String?에 대한 String::class)의 클래스를 반환합니다. value is T 확인은 null을 고려합니다: T = String?이면 null is T = true입니다.
최소한입니다. reified는 리플렉션을 사용하지 않습니다 — 컴파일러가 인라인 단계에서 구체적인 타입을 대책합니다. 바이트코드에서 이는 구체적인 클래스 참조(ldc + checkcast/invokevirtual)입니다. Class<T>를 직접 전달하는 것과 비교하여 오버헤드가 없습니다 — 둔 다 동일한 바이트코드를 생성합니다.
네, reified는 Android에서 활발히 사용됩니다. Bundle.getParcelable<T>(), Intent.getSerializableExtra<T>(), Android KTX의 viewModels<T>() — 이러한 함수들은 명시적으로 Class<T>를 전달하지 않도록 reified를 사용합니다. Google Android Docs(2025)에 따르면, 런타랄에서 타입이 필요한 제네리 API에 reified가 권장됩니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.