
본 포스팅은 CustomResponseHandler 를 구현하다 알게 된 사실에 대해 기록한 것입니다. API 통신을 통해 받아 온 결과가 성공일 수도 있고 실패일 수도 있는데, 기존의 프로젝트 구조에서는 총 두 번의 분기처리가 필요했습니다. Generics 를 이용하여 소정의 목표 (하나의 객체가 성공 및 실패 두 가지 경우의 결과를 모두 필드로 가짐, 발생하는 Exception 을 CoroutineExceptionHandler 에서 처리하게 하여 한 번의 분기처리만이 필요하도록 구현함.) 를 달성할 수 있었습니다. 그 과정 중 reified 를 사용하게 되었고, 이에 관해 복기하며 개념을 다지고자 합니다.
Generics
저는 개인적으로 Generics 야말로 정적 타입 지정 언어의 꽃이라 생각합니다. 컴파일 타임에 타입 검증이 이루어져 프로그램의 안정성을 확보할 수 있고, 불필요한 오버로딩을 줄일 수 있으니까요.
Generics 는 메서드, 클래스, 인터페이스 등 사용처가 다양하고, 강력한 기능을 가집니다. Generic 은 '일반적인' 이라는 뜻이고, -s 가 붙기 때문에 '일반적인 것(들)' 으로 해석할 수 있습니다. 이는 Generics 의 역할에 충실한 네이밍인데, Generics 가 타입을 일반화하기 때문입니다. '일반화' 는 여러 경우의 수가 있음에도 이를 하나의 케이스로 치부하는 행위를 말합니다.
public interface List<out E> : Collection<E>
가장 흔하게 사용되는 Generics 인터페이스인 List 입니다. 타입인 E 에 붙은 out 은 List 라는 인터페이스가 타입 <E> 에 대해 공변적이라는 것을 의미합니다. 해당 키워드에 대해서는 다음 포스팅에 기록해두었습니다.
out, in 제대로 알기
코틀린 프로젝트 내에서 이런 저런 함수들을 타고 타고 가다보면, out, in 이 계속 보입니다. 런타임에 데이터 타입을 결정하는 제네릭(Generic) 입니다. 코틀린의 제네릭는 자바의 그 것과 같지만, o
blothhundr.tistory.com
어쨌든, 해당 인터페이스는 Java 의 Primitive Type 을 제외한 모든 타입을 타입 파라미터로 받을 수 있습니다. 대개 Generics 는 그에 관련된 부가적인 기능을 제공합니다.
reified
Generics 의 경우, 선언해 둔 타입 정보는 컴파일 타임에 타입 검증이 완료되면 지워지므로, 런타임에는 해당 타입에 접근할 수 없습니다. 즉, 코드를 작성할 때에 지정해둔 타입만 사용할 수 있게 됩니다.
그러나, reified 키워드가 타입 파라미터 앞에 붙으면 이야기가 조금 달라집니다. 런타임에도 타입 정보에 접근할 수 있게 됩니다. 런타임에 타입 파라미터에 접근하여 타입을 비교하거나 해당 타입의 인스턴스를 생성하는 등의 작업을 수행할 수 있습니다.
여기서 재미있는 점은, reified 키워드는 inline 키워드와 함께 사용된다는 점입니다.
reified 키워드가 inline 키워드와 함께 사용되는 이유

개발자가 작성한 소스 코드는 컴파일 타임에 자바 바이트코드(.class) 로 변경됩니다. 이 때, 자바 바이트코드에는 클래스, 필드, 메서드, 상수, 명령어 등이 포함되게 되는데, 클래스의 수만큼 생성되며 각 .class 파일은 컴파일되어 독립적으로 존재합니다. 이 때, inline 키워드가 작성된 메서드는 그 메서드의 콜 사이트가 있는 파일에 인라인되며, 여기에 타입 정보가 포함되게 됩니다.
컴파일러가 제네릭을 바이트 코드로 컴파일할 때, 타입에 대한 검증이 끝나면 타입 소거(Type Erasure) 를 통해 타입 정보를 지웁니다. 그 목적은 하위 호환성 유지, 실행 시 성능, 타입 안정성 유지 등이 있습니다.
그러나, 긍정적인 목적으로 고안된 타입 소거로 인해, 예상 밖의 불편한 상황이 연출되곤 합니다. 이를 방지하기 위한 방법으로 고안된 것이 reified(실체화된) 키워드 입니다.
reified 의 작동 방식
컴파일된 코틀린 코드를 다시 자바로 디컴파일하여 작동 방식을 파악해보고자 합니다.
먼저, reified 키워드가 없는 기본 제네릭 함수를 호출해봅니다.
fun main() {
generic<Int>()
generic<String>()
generic<Long>()
}
fun <T> generic() {
}
둘을 자바 코드로 디컴파일 해봅니다.
public final class MainKt {
public static final void main() {
GenericTestKt.generic();
GenericTestKt.generic();
GenericTestKt.generic();
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
public final class GenericTestKt {
public static final void generic() {
}
}
타입 소거로 인해 타입이 모두 지워집니다.
이번엔 generic() 메서드에 inline 과 reified 키워드를 붙여줍니다.
inline fun <reified T> generic() {
println(T::class.java)
}
작동 원리를 파악하기 위해, 타입 파라미터 <T> 의 클래스명을 출력하도록 구성했습니다.
자바 코드로 디컴파일합니다.
@SourceDebugExtension({"SMAP\nMain.kt\nKotlin\n*S Kotlin\n*F\n+ 1 Main.kt\ncom/example/generic/MainKt\n+ 2 GenericTest.kt\ncom/example/generic/GenericTestKt\n*L\n1#1,9:1\n8#2,2:10\n8#2,2:12\n8#2,2:14\n*S KotlinDebug\n*F\n+ 1 Main.kt\ncom/example/generic/ui/MainKt\n*L\n6#1:10,2\n7#1:12,2\n8#1:14,2\n*E\n"})
public final class MainKt {
public static final void main() {
int $i$f$generic = false;
Class var1 = Integer.class;
System.out.println(var1);
$i$f$generic = false;
var1 = String.class;
System.out.println(var1);
$i$f$generic = false;
var1 = Long.class;
System.out.println(var1);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
위 코드의 @SourceDebugExtension 은 코틀린 컴파일러에서 제공하는 어노테이션입니다. 디버깅 시 소스 코드와 관련된 추가적인 정보를 포함하도록 컴파일러에게 지시하는 역할을 수행합니다. 즉, 디버깅 시 정보를 보완하기 위해 사용되는데, 특정 확장 함수나 프로퍼티, 인라인 함수가 소스 코드의 어느 위치에서 정의되었는지를 나타냅니다.
public final class GenericTestKt {
// $FF: synthetic method
public static final void generic() {
int $i$f$generic = 0;
Intrinsics.reifiedOperationMarker(4, "T");
Class var1 = Object.class;
System.out.println(var1);
}
}
There is No Magic
먼저, generic() 메서드입니다. 메서드 내부에 코틀린의 Any 와 같은 역할인 자바의 Object 의 클래스 참조를 var1 로 선언합니다. 그리고 이를 출력하도록 함수가 구성됩니다. 이는 그대로 main() 메서드의 내부 콜 사이트에 붙여 넣어집니다. main() 내부에서는 변수 var1 에 직접 클래스를 로드하여 할당한 뒤 작업을 수행합니다.
특별한 것은 없습니다. 특수한 로직을 통해 작동하는 것이 아니라, 제네릭 메서드의 타입 파라미터 <T> 로 넘겨진 타입에 대한 함수 호출 및 작업을 콜 사이트에서 수행할 수 있게 해주는 것입니다.
reified 키워드는 ide 의 반자동 수정 기능을 통해 처음 사용하게 되었고, 그 이후로도 그랬습니다. 자주 사용하면서도 어떻게 작동하는지 몰랐는데, 이번 기회에 잘 알게 되었습니다.
'Kotlin' 카테고리의 다른 글
Coroutine Details (0) | 2023.06.22 |
---|---|
Kotlin Closure (0) | 2023.05.04 |
by 를 사용한 Kotlin 의 Delegation Pattern (0) | 2022.11.17 |
확장 함수 간단 정리 (0) | 2022.11.15 |
ChannelFlow, CallbackFlow 제대로 알기 (0) | 2022.11.03 |