본문 바로가기

Kotlin

Generics 와 Reified 키워드

Unsplash, Sangga selia.

본 포스팅은 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 키워드와 함께 사용되는 이유

https://www.theappguruz.com/blog/android-compilation-process

 

개발자가 작성한 소스 코드는 컴파일 타임에 자바 바이트코드(.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