ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 10-2. 어노테이션과 리플렉션 : 리플렉션
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 11. 19. 21:26

    10장

    10-1. 어노테이션과 리플렉션 : 어노테이션

    10-2.  어노테이션과 리플렉션 : 리플렉션


    10-2. 리플렉션

     

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

    작성한 프로그램이 컴파일 시점에 알 수 없는 클래스를 다뤄야 하는데,

    어떤 정해진 공통의 계약을 준수해야만 하는 경우 리플렉션이 유용하다.

    예를들면 플러그인으로 클래스를 동적으로 적재하거나,

    멤버들의 시그니처를 알 경우 적재한 클래스에 속한 멤버를 호출하는것이 가능하다.

     

    1) 리플렉션과 API 개요

    리플렉션 관련 클래스는 kotlin.reflect 패키지에 들어있다.

    호출 가능 그룹 / 지정자 그룹으로 크게 두 가지 그룹으로 나눌수 있다.

    호출 가능 그,룹은 프로퍼티와 함수(+생성자)를 표현하며,

    지정자 그룹은 클래스나 타입 파라미터의 런타임 표현을 제공한다.

     

    모든 리플렉션 타입은 KAnnotataedElement의 자손이다.

    이는 함수, 프로퍼티, 클래스 등 구체적인 언어요소에 정의된 어노테이션에 접근하는 기능을 제공한다.

     

    10-1에서 작성했던 코드를 이용해 설명하자면,

    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 클래스와 관련된 어노테이션을 가져오고 싶은 경우

    클래스 리터럴의 annotation 프로퍼티를 통해 정보를 얻을 수 있다.

    fun main() {
        val component = Main::class.annotations
            .filterIsInstance<Component>()
            .firstOrNull() ?: return
        println("Component Name: ${component.name}")
        // Component Name: Core
    
        val depText = component.dependency.componentClasses
            .joinToString { it.simpleName ?: "" }
        println("Dependencies: $depText")
        // Dependencies: IO, Logger
    }

    2) 지정자와 타입

    코틀린 리플렉션에서 지정자는 타입을 정의하는 선언을 의미한다.

    이런 선언은 KClassifier 인터페이스에 의해 표현되며

    이 인터페이스에는 두 가지 독특한 변형(specific variants) 이 있다

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

    위 내용으로 알 수 있듯이 현재 타입 별명을 표현하는 리플렉션API는 없다.

     

    K클래스 인스턴스를 얻는 방법은 두 가지가 있다.

    1) 클래스 리터럴 구문을 사용

    2) kotlin 확장 프로퍼티를 사용해 java.lang.Class의 인스턴스를 KClass로 변환

     

    1번 방법부터 먼저 설명하자면 아래와 같은 클래스 리터럴 구문을 사용할 수 있다.

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

    이 구문은 클래스 뿐만아니라 구체화된 타입 파라미터도 지원한다.

    inline fun <reified T> Any.cast() = this as? T
    
    fun main() {
        val obj: Any = "Hello!"
        println(obj.cast<String>()) 
    //    println(obj as? String)    
    // 내부에서 컴파일러가 생성하는 코드
    }

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

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

     

    KClass를 얻는 두 번째 방법인 kotlin 확장 프로퍼티를 사용해

    java.lang.Class의 인스턴스를 KClass로 변환하는 방법에 대해 살펴보자.

    이 방법은 전체 이름을 갖고 동적으로 찾을 때 유용하다고 한다.

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

    KClass API가 제공하는 것들

    • 대상 클래스에 어떤 변경자가 붙었는지 알아내는 멤버( isAbstract, isCompanion, isFinal, ...)
    • simpleName 프로퍼티 : 소스코드에서 사용되는 간단한 이름 반환
    • isInstanceOf()함수 : 주어진 객체가 수신 객체가 표현하는 클래스의 인스턴스인지 여부 반환
    • constructors, members, nestedClasses, typeParamenters 프로퍼티 존재
    • supertypes 프로퍼티 : KType 인스턴스들의 리스트 반환
    • variance 프로퍼티 : KVariance enum으로 변성을 봔환

     

    코틀린 리플렉션이 KType 인터페이스를 통해 코틀린 타입에 대한 세 가지 성격을 표현한다.

    • isMarkedNullable 프로퍼티 : 널 가능성 (List<String>, List<String>? 구분)
    • classifier 프로퍼티 : 제공하는 지정자(타입 정의 클래스, 인터페이스, 객체 선언) 반환
    • 타입 프로퍼티에 전달된 실제 인자 목록(List<String>이 전달되면 타입 인자 목록은 <String>)

    3) 호출 가능(callable)

    호출 가능 요소란 어떤 결과를 얻기 위해 호출 가능한 함수나 프로퍼티들을 묶은 것을 의미한다.

    리플렉션API에서는 호출가능 요소를 KCallable<out R>이라는 제네릭 인터페이스로 표현한다.

    fun combine(n: Int, s: String) = "$s $n"
    
    fun main() {
        println(::combine.reflect()?.returnType) // kotlin.String
    }

    KCallable이 제공하는 것들

    • 변경자 관련(isAbstratc, isFinal, visibility, ...)
    • isOptional 프로퍼티 : 파라미터에 디폴트 값 존재여부 반환
    • kind 프로퍼티 : KParameter 인스턴스의 값 관련
    • KProperty 인터페이스 : 프로퍼티에만 있는 변경자를 검사하는 프로퍼티 제공

     

    KFunction 리플렉션 타입은 함수나 생성자를 표현하는 리플렉션 타입이다.

    이 인터페이스가 제공하는 멤버는 변경자 검사를 위한 프로퍼티인데, 모두 함수에만 적용 가능하다.

    (isInfix, isInline, isOperator, isSuspanded)

    KFunction은 어떤 함수 타입도 상속하지 않는데, 이는 다양한 인자 개수를 지원해야 되기 때문이다.

     

    리플렉션을 통하면 가시성이 제한된 호출 가능 요소에 접근할 수 있다.

    경우에 따라 비공개 함수를 호출해야 하고 싶을 때 리플렉션을 통해 호출할 수 있다.

    이 경우 미리 isAccessable 프로퍼티에 true를 대입해야 한다.

    false나 값을 할당하지 않은 경우 런타임 에러가 발생한다.

    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 = false // ERROR : IllegalCallableAccessException
        secretProperty.isAccessible = true
        println(secretProperty.get(secretHolder)) // Secret
    }

    어노테이션과 리플렉션에 대한 단원이었다.

    사실 어떻게 동작하는지는 알겠는데, 어디에 사용하는지는 명확히 감이 안잡히긴 했다.

    어노테이션을 만들고, 사용하고, 런타임때 어노테이션을 이용해 어떤 값에 접근하는 방법 정도로 이해했다.

    다음장은 도메인 특화 언어이다. 사실 이번장은 부담이 좀 없었는데 다음장은 부담이 좀 된다 분량이 많기도하고.. 

    그래도 어떻게 어거지로라도 꾸준히 하고 있는 걸 보니 신기하긴 한데, 다음장도 꾸역꾸역 해봐야겠다.

     

    반응형

    댓글

Designed by Tistory.