반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[코틀린 완벽 가이드] 10장 : 애너테이션과 리플렉션 본문

Kotlin/코틀린 완벽 가이드

[코틀린 완벽 가이드] 10장 : 애너테이션과 리플렉션

어리둥절범고래 2022. 11. 15. 09:30
반응형

1. 애너테이션

애너테이션은 커스텀 메타데이터를 정의하고 이 메타데이터를 소스코드 상의 선언, 식, 전체 파일 등의 요소에 엮을 수 있는 수단이다.

 


1. 애너테이션 클래스 정의하고 사용하기

 

애너테이션은 선언의 앞쪽 변경자 위치에 @이 붙은 애너테이션의 이름을 놓는다.

import org.junit.Test

class MyTestCase {
    @Test
    fun testOnePlusOne() {
        assert(1 + 1 == 2)
    }
}

 

코틀린은 애너테이션을 식에 적용할 수 있으며, 애너테이션을 여럿 붙이고 싶다면 각괄호([ ])로 애너테이션들을 감쌀 수 있다.

val s = @Suppress("UNCHECKED_CAST") object as List<String> // 식에 적용

@[Synchronized Strictfp] // @Synchronized @Strictfp 와 같다
fun main() {}

 

 

애너테이션을 주생성자에 적용하고 싶다면 주생성자 인자 목록 앞에 constructor 키워드를 붙여야 한다.

class A @MyAnnotation constructor()

 

 

애너테이션을 정의하려면 클래스 앞에 annotation이라는 변경자를 붙여야 한다.

annotation class MyAnnotation

@MyAnnotation fun annotationFun() { }

 

 

자바는 애너테이션이 인터페이스로 구성되지만 코틀린은 특별한 종류의 클래스로 구성된다.

애너테이션 클래스에는 멤버나 부생성자, 초기화 코드를 넣을 수 없으며 내포된 클래스, 인터페이스, 객체는 넣을 수 있다.

annotation class MyAnnotation {
    companion object {
        val text = "123"
    }
    val text = "???" // Error: Members are not allowed in annotation class
}

 

애너테이션에 생성자 파라미터를 통해 커스텀 애트리뷰트를 추가할 수 있다. 파라미터는 항상 val 로 선언해야 한다.

annotation class MyAnnotation(val text: String)

@MyAnnotation("Some useful info") fun annotationFun() { }

 

자바 애너테이션에서는 애트리뷰트를 파라미터가 없는 메서드 형태로 지정해야 한다.

코틀린에서는 생성자 파라미터가 프로퍼티 역할을 함께 담당한다. 일반적인 생성자와 마찬가지로 디폴트 값이나 가변 인자를 사용할 수 있다.

annotation class Dependency(val arg: String, val componentNames: String="Core")
annotation class Component(val name: String = "Core")

@Component("I/O")
class IO

@Component("Log")
@Dependency("I/O")
class Logger

@Component
@Dependency("I/O", "Log")
class Main

  

 

코틀린 애너테이션은 일반적인 클래스 방식으로 인스턴스를 만들 수 없으며 @ 구문을 사용해야만 애너테이션 인스턴스를 생성할 수 있다. 런타임까지 유지되는 애너테이션의 실제 인스턴스를 얻기 위해서는 리플렉션 API를 사용해야 한다.

 

현재는 일반적인 방식으로 애너테이션 클래스의 인스턴스를 생성하면 리플렉션 API로 인스턴스를 생성해준다.

annotation class Component(val name: String = "Core")

val ioComponent = Component("IO")
println(ioComponent.annotationClass) // class Component

 

 

애너테이션 클래스에는 상위 타입을 명시할 수 없으며, 애너테이션 클래스를 상속하는 클래스를 정의할 수도 없다.

모든 애너테이션은 상위 타입으로 Any 클래스와 빈 애너테이션 인터페이스를 자동으로 상속한다.

 

애너테이션 인자에 식을 넣을 수 없으며, 널이 올 수 없다.

또한, 파라미터로 사용할 수 있는 타입의 종류가 제한되어있다.

  • Int, Boolean, Double 등 원시 타입
  • String
  • 이넘
  • 다른 애너테이션
  • 클래스 리터럴
  • 위에 나열한 타입들로 이뤄진 배열

 

다른 애너테이션을 인자로 사용하는 경우에는 @를 안붙여도 된다.

annotation class Dependency(vararg val componentNames: String)
annotation class Component(
    val name: String = "Core",
    val dependency: Dependency = Dependency()
)

@Component("I/O")
class IO

@Component("Log", Dependency("I/O"))
class Logger

@Component(dependency = Dependency("I/O", "Log"))
class Main

 

애너테이션 파라미터로 vararg 대신 명시적인 배열 타입을 사용할 수도 있다.

annotation class Dependency(val componentNames: Array<String>)
@Component(dependency = Dependency(["I/O", "Log"])) // Dependency(arrayOf("I/O", "Log"))

 

클래스 리터럴을 사용하면 KClass 타입의 리플렉션 객체로 클래스에 대한 표현을 얻을 수 있다.

클래스 리터럴은 ::class를 붙여서 만들 수 있다.

annotation class Dependency(vararg val componentClasses: KClass<*>)
annotation class Component(
    val name: String = "Core",
    val dependency: Dependency = Dependency()
)

@Component("I/O")
class IO

@Component("Log", Dependency(IO::class))
class Logger

@Component(dependency = Dependency(IO::class, Logger::class))
class Main

 

 

코틀린 소스코드에서 여러 언어 요소가 함축돼 있는 선언에 대해 애너테이션이 붙을 수도 있다.

class Person(val name: String)

이 코드의 val name: String은 생성자 파라미터와 게터가 있는 클래스 프로퍼티, 프로퍼티 값을 저장하기 위한 뒷받침하는 필드 선언을 짧게 줄인 코드이다. 이런 요소 각각에 대해 애너테이션을 붙일 수 있으며 사용하는 시점에 어떤 대상에 대해 애너테이션을 붙이는지 지정할 수 있다.

 

이런 사용 지점 대상은 '@지정할 대상:애너테이션 이름'과 같은 모양으로 사용한다.

다양한 프로퍼티 구성 요소와 연관되어있는 사용 지점 대상은 최상위 클래스 수준의 프로퍼티, 주생성자의 val / var파라미터에 대해서도 이런 대상을 지정할 수 있다.

  • property: 프로퍼티 자체를 대상으로 한다.
  • field: 뒷받침하는 필드를 대상으로 한다(뒷받침하는 필드가 있다면).
  • get: 프로퍼티 게터를 대상으로 한다.
  • set: 프로퍼티 세터를 대상으로 한다.
  • param: 생성자 파라미터를 대상으로 한다(val / var 가 붙은 파라미터만 대상으로 삼을 수 있다).
  • setparam: 프로퍼티 세터의 파라미터를 대상으로 한다(가변 프로퍼티에만).
  • delegate: 위임 객체를 저장하는 필드를 대상으로 한다(위임 프로퍼티에만).
class PersonA(@get:A val name: String // 게터에 대한 예
class PersonB(@set:[A B] val name: String // 마찬가지로  [ ] 사용 가능

 

 

receiver라는 대상을 사용하면 확장 함수나 프로퍼티의 수신 객체에 애너테이션을 붙일 수 있다.

class Person(val firstName: String, val familyName: String)

fun @receiver:A Person.fullName() = "$firstName $familyName"

 

file이라는 대상을 사용해 전체 파일에 대해 애너테이션을 붙일 수 있다.

@file:JvmName("MyClass") // 이 줄은 파일 맨 앞에 있어야 함

fun main() {
    println("main() in MyClass")    
}

 

 


2. 내장 애너테이션

 

코틀린은 컴파일러 수준에서 의미를 가지는 몇 가지 내장 애너테이션을 제공한다. 이런 애너테이션은 애너테이션 클래스 자체에 적용할 수 있고, 이런 애너테이션을 통해 대상 애너테이션의 사용 방법을 바꿀 수 있다.

 

@Retention 애너테이션은 애너테이션의 저장되고 유지되는 방식을 제어한다.

AnnotationRetention 이넘 클래스에 적용된 세 가지 중 한 가지 저장 방식을 지정할 수 있다.

  • SOURCE: 컴파일 시점에만 존재하며 컴파일러의 바이너리 출력에는 저장되지 않는다.
  • BINARY: 컴파일러의 바이너리 출력에 저장되지만, 런타임에 리플렉션 API로 관찰할 수는 없다.
  • RUNTIME: 컴파일러의 바이너리 출력에 저장되며 런타임에 리플렉션 API를 통해 관찰할 수도 있다.

디폴트로 코틀린 애너테이션은 RUNTIME으로 유지 시점이 정의된다. 따라서 리플렉션 API에서 코틀린 애너테이션을 찾지 못할 염려는 없다. 하지만 현재는 식에 대해 붙은 애너테이션의 경우 런타임까지 유지되지 못한다. 그러므로 식에 붙는 애너테이션에 대해 BINARY나 RUNTIME을 지정하는 것은 금지되어 있다.

// Error: Expression annotations with retention other than SOURCE are prohibited
@Target(AnnotationTarget.EXPRESSION)
annotation class NeedToRefactor

이런 경우 명시적으로 SOURCE 유지 시점을 지정해야 한다.

@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.EXPRESSION)
annotation class NeedToRefactor // ok

 

 

@Repeatable이 붙은 애너테이션을 같은 언어 요소에 두 번 이상 반복 적용할 수 있다.

@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class Author(val name: String)

@Author("John")
@Author("Harray")
class Services

기본적으로 애너테이션을 반복 적용할 수 없으며 반복 적용할 수 없는 애너테이션을 여러 번 적용하면 오류가 발생한다.

@Deprecated("Deprecated")
@Deprecated("Even more deprecated") // error: This annotation is not repeatable
class OldClass

현재는 반복할 수 있는 애너테이션을 런타임까지 유지할 수 없다. 따라서 반복할 수 있는 애너테이션의 유지 시점을 반드시 SOURCE로 명시해야 한다.

 

 

@MustBeDocumented는 애너테이션을 문서에 꼭 포함시키라는 뜻이다. 애너테이션도 공개 API의 일부인 경우 이 애너테이션을 붙인다.

 

@Target은 애너테이션을 어떤 언어 요소에 붙일 수 있는지 지정한다. AnnotationTarget 이넘에 정의된 다음 상수들을 vararg로 지정하면 된다.

  • CLASS: 클래스, 인터페이스, 객체에 붙일 수 있다(애너테이션 클래스 포함).
  • ANNOTATION_CLASS: 애너테이션 클래스에 붙일 수 있다.
  • TYPEALIAS: 타입 별명 정의에 붙일 수 있다.
  • PROPERTY: 주생성자에 정의된 val / var 프로퍼티를 포함해, 프로퍼티에 붙일 수 있다.
  • FIELD: 프로퍼티를 뒷받침하는 필드에 붙일 수 있다.
  • LOCAL_VARIABLE: 지역 변수에 붙일 수 있다(파라미터 제외).
  • VALUE_PARAMETER: 생성자, 함수, 프로퍼티 세터의 파라미터에 붙일 수 있다.
  • CONSTRUCTOR: 주생성자나 부생성자에 붙일 수 있다.
  • FUNCTION: 람다나 익명 함수 포함, 함수에 붙일 수 있다.
  • PROPERTY_GETTER/PROPERTY_SETTER: 프로퍼티 게터/프로퍼티 세터에 붙일 수 있다.
  • FILE: 파일에 붙일 수 있다.
  • TYPE: 제너릭 선언의 파라미터 타입을 제외, 변수의 타입, 함수의 파라미터 타입, 반환 타입을 포함힌 타입 지정에 붙일 수 있다.
  • EXPRESSION: 식에 붙일 수 있다.

 

 

다음 애너테이션은 이름이 같은 자바 변경자와 같은 역할을 한다.

  • @StrictFp: 부동소수점 연산의 정밀도를 제한해서 여러 다른 플랫폼 간의 부동소수점 연산 이식성을 높여준다.
  • @Synchronized: 애너테이션이 붙은 함수나 프로퍼티 접근자의 본문에 진입하기전에 모니터를 획득하고 본문 수행 후 모니터를 헤제하게 한다.
  • @Voatile: 애너테이션이 붙은 뒷받침하는 필드를 변경한 내용을 즉시 다른 스레드에서 관찰할 수 있게 해준다.
  • @Transient: 애너테이션이 붙은 필드를 직렬화 메커니즘이 무시한다.

 

 

@Suppress 애너테이션을 사용하면 지정한 이름의 컴파일러 경고를 표시하지 않게 할 수 있다.

val strings = listOf<Any>("1", "2", "3")
val numbers = listOf<Any>(1, 2, 3)

// 경고 표시되지 않음
val s = @Suppress("UNCHECKED_CAST") (strings as List<String>)[0]
// Warning: Unchecked cast: List<Any> to List<Number>
val n = (numbers as List<Number>)[1]

이 애너테이션은 자신이 적용된 요소 내부에 있는 모든 코드에 적용되며 파일을 사용 지점 대상으로 지정하면 파일 전체에서 경로를 없앨 수도 있다.

@file:Suppress("UNCHECKED_CAST")

val strings = listOf<Any>("1", "2", "3")
val numbers = listOf<Any>(1, 2, 3)

// 경고 표시되지 않음
fun takeString() = (strings as List<String>)[0]
fun takeNumber() = (numbers as List<Number>)[1] 
fun main() {
    println(takeString() + takeNumber()) // 12
}

 

 

@Deprecated 애너테이션은 어떤 선언을 사용 금지 예정(deprecated)이라고 선언하면, 이 선언을 사용하지 않는 것을 클라이언트 코드에게 권장한다. 일반적으로 왜 이 선언이 사용 금지 예정인지 알려주고, 이 선언 대신 쓸 수 있는 대안을 알려준다.

 

Deprecated 애너테이션 안에서만 사용할 수 있는 ReplaceWith 애너테이션은 사용 금지 예정 대상을 대신할 수 있는 식을 문자열로 지정해준다.

@Deprecated(
    "Use readInt() instead", // 메시지
    ReplaceWith("readInt()") // 대안
)

fun readNum() = readLine()!!.toInt()

ReplaceWith 애너테이션의 정의를 살펴보면 다음과 같다.

@Target()
@Retention(BINARY)
@MustBeDocumented
public annotation class ReplaceWith(val expression: String, vararg val imports: String)

 

이 애너테이션이 추가로 받는 vararg 파라미터는 사용 금지 예정인 식을 대신할 때 필요한 임포트문 목록을 지정하기 위한 것이다.

 

 

다른 기능으로, 사용 금지 예정의 심각성을 지정할 수도 있다. 이때 DeprecationLevel 이넘을 사용한다.

  • WARNING: 사용 금지 예정이 붙은 선언을 사용하면 경로를 표시하며, 이 동작이 디폴트다.
  • ERROR: 사용 금지 예정이 붙은 선언을 사용하면 컴파일 오류로 처리한다.
  • HIDDEN: 사용 금지 예정이 붙은 선언에 접근하지 못하게 막는다.

 

 

 


2. 리플렉션

리플렉션 API는 클래스, 함수, 프로퍼티의 런타임 표현에 접근할 수 있게 해주는 타입, 함수, 프로퍼티 모음이다.


1. 리플랙션 API 개요

 

리플렉션 관련 클래스는 크게 두 가지 그룹으로 나눌 수 있다.

  • 호출 가능 그룹: 프로퍼티와 함수(생성자 포함)를 표현
  • 지정자 그룹: 클래스나 타입 파라미터의 런타임 표현을 제공

 

모든 리플렉션 타입은 KAnnotatedElement의 자손이며 KAnnotatedElement는 함수, 프로퍼티, 클래스 등 구체적인 언어 요소에 정의된 애너테이션에 접근하는 기능을 제공한다.

 

KAnnotated Element에는 프로퍼티가 하나뿐이다. 이 프로퍼티는 애너테이션 인스턴스로 이뤄진 리스트다.

public interface KAnnotatedElement {
    /**
     * Annotations which are present on this element.
     */
    public val annotations: List<Annotation>
}

 

 

앞에서 본 @Component / @Dependency 예제를 다시 살펴보면,

import kotlin.reflect.KClass

annotation class Dependency(vararg val componentClasses: KClass<*>)

annotation class Component(
    val name: String = "Core",
    val dependency: Dependency = Dependency()
)

@Component("I/O")
class IO

@Component("Log", Dependency(IO::class))
class Logger

@Component(dependency = Dependency(IO::class, Logger::class))
class Main

여기서 Main 클래스와 연관된 애너테이션을 가져오고 싶다면, 클래스 리터럴의 annotations 프로퍼티를 통해 정보를 얻을 수 있다.

fun main() {
    val component = Main::class.annotations
        .filterIsInstance<Component>()
        .firstOrNull() ?: return

    println("Component name: ${component.name}")

    val depText = component.dependency.componentClasses.joinToString { it.simpleName ?: "" }
    println("Dependencies: $depText")
}
/*
    Component name: Core
    Dependencies: IO, Logger
 */

 

 


2. 지정자와 타입

 

코틀린 리플렉션에서 지정자는 타입을 정의하는 선언이다. 이런 선언은 KClassifier 인터페이스에 의해 표현된다. 이 인터페이스 자체에는 아무 멤버도 들어있지 않으며 오직 두 가지 구체적인 변종이 있다.

  • KClass<T>: 컴파일시점에 T 타입인 클래스나 인터페이스, 객체 선언을 런타임에 표현한다.
  • KTypeParameter: 어떤 제네릭선언의 타입 파라미터를 표현한다.

타입 별명(Typealias)을 표현하는 리플렉션 API는 없다. 타입별명에 애너테이션을 적용해도 런타임에 이를 얻을 수 없다.

 

 

KClass

 

KClass 인스턴스를 얻는 방법은 두 가지다.

 

첫 번째는 클래스 리터럴 구문(::class)을 사용하는 방법이다.

println(String::class.isFinal) // true

리터럴 구문은 클래스뿐 아니라 구체화한 타입 파라미터도 지원한다. 제네릭 인라인 함수의 타입 파라미터터를 구체화 할 수 있으며, 컴파일러가 함수 호출 지점에 함수 본문을 인라인해주면서 이런 타입 파라미터의 타입을 실제 타입으로 대치해준다. 예를 들어 cast() 함수를 정의하면,

inline fun <reified T> Any.cast() = this as? T

이 함수를 다음과 같이 호출한다.

val obj: Any = "Hello"
println(obj.cast<String>())

내부에서 컴파일러는 다음과 같은 코드를 생성한다.

val obj: Any = "Hello"
println(obj as? String)

 

::class 구문을 사용하면 임의의 식의 결괏값에 대한 런타임 클래스를 얻을 수 있다.

println((1 + 2)::class) // class kotlin.Int
println("abc"::class)   // class kotlin.String

 

 

KClass 인스턴스를 얻는 두 번째 방법은 코틀린 확장 프로퍼티를 사용해 java.lang.Class 인스턴스를 KClass로 변환하는 것이다. 전체 이름을 갖고 클래스를 동적으로 찾을 때 유용하다.

 

코틀린 리플렉션은 클래스를 검색하는 API를 제공하지 않으므로 적절한 클래스 검색 방식을 사용해야 한다.

val stringClass = Class.forName("java.lang.String").kotlin
println(stringClass.isInstance("Hello")) // true

// 반대로도 가능하다.
print(String::class.java) // class java.lang.String

 

KClass API 내부를 보면, KClass의 대상 클래스에 어떤 변경자가 붙어있는지를 알아낼 수 있게 해주는 프로퍼티를 제공한다.

val isFinal: Boolean
val isOpen: Boolean
val isAbstract: Boolean
val isSealed: Boolean
val isData: Boolean
val isInner: Boolean
val isCompanion: Boolean
val isFun: Boolean
val isValue: Boolean

visibility라는 프로퍼티는 KVisibility 이넘으로 클래스 선언의 가시성 수준을돌려준다.

val visibility: KVisibility?

enum class KVisibility {
    PUBLIC,
    PROTECTED,
    INTERNAL,
    PRIVATE,
}

소스코드에서 가시성을 표현할 수 없다면 visibility값이 null이다.

 

 

다음 프로퍼티들은 클래스 이름을 제공한다.

val simpleName: String?
val qualifiedName: String?
  • simpleName:  소스코드에서 사용되는 간단한 이름을 반환한다. 클래스 이름이 없다면 결과는 null이다.
  • qualifiedName: 클래스 전체 이름을 얻을 수 있다. 전체 이름에는 패키지의 경로가 들어간다. 클래스가 이름이 없거나 지역 클래스이거나 로컬 클래스 안에 내포된 클래스인 경우, 최상위 경로에서 접근할 방법이 없으므로 이런 클래스에는 전체 이름이 없으므로 null을 돌려준다.

jvmName 확장 프로퍼티를 사용하면 자바 관점에서 보는 클래스 전체 이름을 돌려준다.

println(Any::class.qualifiedName) // kotlin.Any
println(Any::class.jvmName)       // java.lang.Object

 

 

isInstance() 함수는 value가 수신 객체의 인스턴스면 true를 반환한다.

println(String::class.isInstance(""))   // ture
println(String::class.isInstance(12))   // false
println(String::class.isInstance(null)) // false

 

 

다음으로 이 프로퍼티들은 KClass 멤버 선언에 접근할 수 있게 해준다.

  • constructors: 주생성자의 부생성자들을 KFunction 타입의 인스턴스로 돌려준다.
  • members: KCallable 인스턴스로 표현되는 멤버 함수와 프로퍼티 표현의 컬렉션을 돌려준다.
  • nestedClasses: 내포된 클래스와 객체들로 이뤄진 컬렉션으로 동반 객체도 포함된다.
  • typeParameters: KTypeParameter에 의해 표현되는 타입 파라미터로 이뤄진 리스트다.

 

 

다음은 리플렉션을 사용해  Person 클래스의 인스턴스를 만들고, 그 인스턴스의 fullName() 함수를 호출한다.

class Person(val firstName: String, val familyName: String) {
    fun fullName(familyFirst: Boolean): String = if (familyFirst) {
        "$familyName $firstName"
    }else {
        "$firstName $familyName"
    }
}

fun main() {
    val personClass = Class.forName("Person").kotlin
    val person = personClass.constructors.first().call("John","Doe")
    val fullNameFun = personClass.members.first { it.name == "fullName" }

    println(fullNameFun.call(person, false)) // John Doe
}

KClass가 객체 선언을 표현하는 경우 constructors 프로퍼티는 항상 빈 컬렉션을 반환한다. 실제 인스턴스를 얻고 싶으면objectInstance 프로퍼티를 사용해야 한다.

object O {
    val text = "Singleton"
}

fun main() {
    println(O::class.objectInstance!!.text) // Singleton
}

 

마지막으로 봉인된 클래스(isSealed == true)의 경우 sealedSubclasses 프로퍼티를 통해 직접적인 상속자로 이뤄진 리스트를 얻을 수 있다.

 

 

KClass에서 얻을 수 있는 다른 종류의 정보로 supertypes 프로퍼티를 통해 얻을 수 있는 KType 인스턴스의 리스트를 들 수 있다.

open class GrandParent
open class Parent : GrandParent()
interface IParent
class Child : Parent(), IParent

fun main() {
    println(Child::class.supertypes) // [Parent, IParent]
}

supertypes 프로퍼티는 클래스가 직접 상속한 상위 타입만 돌려준다(GrandParent 제외). 따라서 간접적인 상위 클래스도모두 포함하고 싶다면 별도의 상속 그래프 순회를 수행해야 한다.

 

 

KClassifier를 상속받는 KTypeParameter 인터페이스는 KClass와 비교할 때 단순하며 프로퍼티를 네 개만 제공한다.

val name: String
val upperBounds: List<KType>
val variance: KVariance
val isReified: Boolean

 

upperBounds 프로퍼티는 KClass의 supertypes 프로퍼티와 비슷하게 상위 바운드 타입으로 이뤄진 리스트를 돌려준다.

interface MyMap<K: Any, out V>

fun main() {
    val parameters = MyMap::class.typeParameters
    // K: [kotlin.Any], V: [kotlin.Any?]
    println(parameters.joinToString { "${it.name}: ${it.upperBounds}" })
}

 

variance 프로퍼티는 KVariance 이넘으로 변성을 돌려준다. 이 이넘은 타입 파라미터의 선언 지점 변성 종류를 표현한다.

enum class KVariance { INVARIANT, IN, OUT }

 

코틀린 리플렉션이 KType 인터페이스를 통해 어떻게 타입을 표현하는지 살펴보자. 코틀린 타입은 다음과 같은 세 가지 성격을 지닌다.

  • isMarkedNullable 프로퍼티가 제공하는 널 가능성, 예를 들면 이를 통해 List<String>과 List<String>?를 구분할 수 있다.
  • classifier 프로퍼티를 통해 제공하는 지정자. 지정자는 타입을 정의하는 클래스, 인터페이스나 객체 선언을 가리킨다. 예를 들어 List<String>에서 List 부분을 가리키는 리플렉션 요소가 지정자다.
  • 타입 프로퍼티에 전달된 실제 타입 인자 목록, 예를 들어 List<String>이면 <String>, Map<Int, Boolean>이면 <Int, Boolean>이 타입 인자 목록이다.

타입 인자가 타입 자체와 타입의 사용 지점 변성을 함께 포함하는 KTypeProjection 인터페이스에 의해 표현될 수도 있다.

val type: kotlin.reflect.KType?
val variance: kotlin.reflect.KVariance?

 

 


3. 호출 가능

 

호출 가능(callable) 요소라는 개념은 어떤 결과를 얻기 위해 호출할 수 있는 함수나 프로퍼티를 함께 묶어준다.

리플렉션 API에서는 KCallable<out R>이라는 제네릭 인터페이스를 통해 호출 가능 요소를 표현한다. 여기서 R은 함수의 반환 타입이거나 프로퍼티의 타입에 해당한다.

 

KCallable 인스턴스를 얻는 방법은 호출 기능 참조를 사용하는 방식이 있다.

fun combine(n: Int, s: String) = "$s$n"

fun main() {
    println(::combine.returnType) // kotlin.String
}

 

 

KCallable이 제공하는 멤버들은 KClass와 마찬가지로 어떤 변경자가 붙어있는지 알아낼 수 있는 프로퍼티들이 존재한다.

val isAbstract: Boolean
val isFinal: Boolean
val isOpen: Boolean
val isSuspend: Boolean
val visibility: KVisibility?

isSuspend에 해당하는 suspend 변경자는 일시 중단 가능한 계산(suspandable computation)을 지원하는 호출 가능 객체에 사용된다.

 

KCallable에는 프로퍼티나 함수의 시그니처를 표현하는 프로퍼티도 존재한다.

val name: String
val typeParameters: List<KTypeParameter>
val parameters: List<KParameter>
val returnType: KType

멤버와 확장의 경우 첫 번째 파라미터는 수신 객체로 예약돼 있다. 호출 가능 요소가 멤버인 동시에 확장이라면 두 번째 파라미터도 다른 수신 객체로 예약돼 있다. 예를 들면,

import kotlin.reflect.KCallable

val simpleVal = 1
val Int.extVal get() = this

class A {
    val Int.memberExtVal get() = this
}

fun main() {
    fun printParams(callable: KCallable<*>) {
        println(
            callable.parameters.joinToString(prefix = "[", postfix = "]") {
                it.type.toString()
            }
        )
    }
    // []
    printParams(::simpleVal)
    // [kotlin.Int]
    printParams(Int::extVal)
    // [A, kotlin.Int]
    printParams(A::class.members.first { it.name == "memberExtVal"} )
}

 

 

KParameter 인터페이스는 멤버 및 확장 선언의 수신 객체나 함수/생성자의 파라미터에 대한 정보를 포함한다.

val index: Int
val isOptional: Boolean
val isVararg: Boolean
val name: String?
val type: KType
val kind: KParameter

isOptional 프로퍼티는 파라미터에 디폴트 값이 있는지 여부를 돌려준다.

kind 프로퍼티는 KParameter 인스턴스가 일반적인 값에 해당하는지, 아니면 디스패치나 확장의 수신 객체인지를 알려준다. 

  • INSTANCE: 멤버 선언의 디스패치 수신 객체
  • EXTENSION_RECEIVER: 확장 선언의 확장 수신 객체
  • VALUE: 일반적인 값

 

KCallable에는 이 호출 가능 요소가 표현하는 호출 가능한 선언을 동적으로 호출할 수 있게 해주는 call() 멤버 함수가 들어있다.

fun call(vararg args: Any?): R

 

호출 가능 요소가 함수로부터 만들어진 경우 call()은 함수를 호출하며 프로퍼티라면 게터가 호출된다.

class Person(val firstName: String, val familyName: String)

fun main() {
    val person = Person("John","Doe")
    val personClass = person::class
    val firstName = personClass.members.first { it.name == "firstName" }

    println(firstName.call(person)) // John
}

 

호출 가능 요소를 호출하는 다른 방법으로 callBy() 함수가 있다. 이 함수를 사용하면 맴 형태로 인자를 넘길 수 있다.

fun callBy(args: Map<KParameter, Any?>): R

 

KProperty 인터페이스는 프로퍼티에만 있는 변경자를 검사하는 프로퍼티를 추가로 제공한다.

val isConst: Boolean
val isLateinit: Boolean

 

프로퍼티 게터를 KFunction 타입의 인스턴스를 통해 접근할 수도 있다.

val myValue = 1

fun main() {
    println(::myValue.getter()) // 1
}

 

KMutableProperty는 KProperty에 세터를 추가해준다.

var myValue = 1

fun main() {
    ::myValue.setter(2)
    println(myValue) // 2
}

 

 

KProperty에도 KProperty0, KProperty1, KProperty2라는 하위 타입이 있다.

  • KProperty0: 수신 객체가 없는 경우
  • KProperty1: 수신 객체가 하나인 경우 (디스패치이거나 확장인 경우)
  • KProperty2: 수신 객체가 둘인 경우 (멤버인 확장의 경우)

이런 하위 타입은 자신이 속한 유형에 따라 다른 함수 타입으로 게터 타입과 세터 타입을 세분화해준다.

 

 

마지막으로 KFunction이다. 이 타입은 함수나 생성자를 표현한다. 이 인터페이스에는 함수에 적용 가능한 변경자 검사를 위한 프로퍼티만 존재한다.

val isInfix: Boolean
val isInline: Boolean
val isOperator: Boolean
val isSuspend: Boolean

KFunction 자체는 다양한 인자 개수를 지원해야 하므로 어떤 함수 타입도 상속하지 않는다.

일부 함수 타입은 좀 더 구체적인 KFunction의 하위 타입을 통해 구현될 수 있으며, 이런 하위 타입의 예로 KProperty 0,1,2가 있다.

또한 호출 가능 참조가 항상 적절한 함수 타입을 준수한다.

import kotlin.reflect.KFunction2

fun combine(n: Int, s: String) = "$s$n"

fun main() {
    val f: KFunction2<Int, String, String> = ::combine
    println(f(1, "2")) // 21
}

 

이 예제의 호출 가능 참조는 KFunction2<Int, String, String>이며, 이는 (Int, String) -> String의 하위 타입이다. 하지만 KProperty0나 다른 비슷한 타입들과 달리 KFunction0/KFunction1/..등의 타입은 컴파일 시점에만 존재한다. 런타임에 이들은 합성 클래스에 의해 표현되며, 이는 람다를 표현하기 위해 합성 객체를 사용하는 것과도 비슷하다.

 

 

리플렉션을 통하면 가시성이 제한된 호출 가능 요소에 접근할 수도 있다. 경우에 따라 비공개 함수를 리플렉션을 통해 호출해야 할 때가 있다. 자바에서 이런 시도를 하면 예외가 발생하며 이럴 때는 미리 setAccessible(true)를 호출해서 해당 요소에 접근할 수 있게 설정해야 한다.

import kotlin.reflect.KProperty1
import kotlin.reflect.jvm.isAccessible

class SecretHolder(private val secret: String)

fun main() {
    val secretHolder = SecretHolder("Secret")
    val secretProperty = secretHolder::class.members
        .first { it.name == "secret" } as KProperty1<SecretHolder, String>
    
    secretProperty.isAccessible = true
    println(secretProperty.get(secretHolder))
}

 

 


마무리 정리

 

코틀린 리플랙션 API

실행 시점에 (동적으로) 객체의 프로퍼티와 메소드에 접근할 수 있게 해주는 방법이다. 보통 객체의 메소드나 프로퍼티에 접근할 때는 프로그램 소스코드 안에 구체적인 선언이 있는 메소드나 프로퍼티 이름을 사용하며, 컴파일러는 그런 이름이 실제로 가리키는 선언을 컴파일 시점에 찾아내서 해당하는 선언이 실제 존재함을 보장한다.

 

  • KClass
    • 클래스를 표현한다.
    • java.lang.Class에 해당하는 KClass
    • 모든 선언 열거, 상위 클래스 얻는 등의 작업 가능
    class Person(val name: String, val age: Int)
    
    val person = Person("Seungmin", 27)
    
    val kClass = Person::class // 클래스로부터 KClass 얻기
    val kClass = person.javaClass.kotlin // 인스턴스로부터 KClass 얻기
    
  • KCallable
    • 함수, 프로퍼티의 공통 상위 인터페이스
    • call 인터페이스를 제공해 가변 인자와 가변 반환을 할 수 있다.
  • KFunction
    • 함수 표현
    • invoke 함수를 제공해서 컴파일 타임에 인자 개수와 타입에 대한 체크를 할 수 있다.
    • KFunction1<Int, Unit>: 이런 식으로 반환 값 타입 정보가 들어있는 식으로 활용 가능
    • KFunctionN은 컴파일러가 생성한 합성 타입이므로, 이런 타입의 정의를 미리 찾을 수 없을 것이다. 컴파일러가 원하는 만큼 생성하므로 kotlin-runtime.jar 크기를 줄일 수 있고 함수 파라미터 개수에 대한 인위적인 제약을 피한다.
  • KProperty
    • 프로퍼티 표현 (함수의 로컬 변수에는 접근할 수 없다)
    • get 함수를 제공해서 프로퍼티 값을 얻을 수 있다.
    • KProperty0 최상위 프로퍼티 → 인자 없는 get 함수로 값 받아오기
    • KProperty1 멤버 프로퍼티 → 객체에 속해 있는 프로퍼티이므로 값을 받아오려면 객체 인스턴스를 넘겨야한다. 즉 인자가 1개 있는 get함수 제공
반응형