Kotlin 공부(kotlin in action)/10장

2023 01 17 코틀린 공부 - 애노테이션과 리플렉션-1

posite 2023. 1. 17. 23:39

10장 애노테이션과 리플렉션

 

어떤 함수를 호출할 때 함수의 이름, 파라미터등을 알아야했으나 애노테이션과 리플렉션을 사용하면 그런 제약을 벗어날 수 있다. 

애노테이션을 적용하는 방법은 적용하려는 대상 앞에 붙이면 된다. @와 애노테이션 이름으로 이루어진다. JUnit의 테스트 메서드 앞에는 @Test를 붙여야 한다. 

 

클래스를 애노테이션 인자로 지정할 때 @MyAnnotatioin(MyClass::class)처럼 클래스 이름 뒤에 ::class를 넣어야 한다.

다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 @를 넣지 않아야 한다.

배열을 인자로 지정하려면 @RequestMapping(path = arrayOf("  ", "")처럼 arrayOf를 사용한다.

 

애노테이션 인자를 컴파일 시점에 알 수 있어야하기때문에 const 변경자를 붙여서 프로퍼티를 인자로 지정할 수 있다. 이유는 const가 붙은 프로퍼티를 컴파일 시점 상수로 취급 하기 때문이다

 

사용 지점 대상 선언으로 애노테이션을 붙일 요소를 정할 수 있다. 대상은 @와 애노테이션 이름 사이에 붙으며 :로 애노테이션 이름과 분리 된다. @get: Rule 에서 get이 대상이 되고 Rule은 애노테이션 이름이다.

사용 지점 대상은 다음과 같다.

property - 프로퍼티 전체 자바에서 선언된 애노테이션에는 이  사용 지점 대상을 사용할 수 없다.

field - 프로퍼티에 의해 생성되는 필드

get - 게터

set - 세터

receiver - 확장 함수나 프로퍼티 수신 객체 파라미터

param - 생성자 파라미터

setparam - 세터 파라미터

delegate - 위임 프로퍼티의 위임 인스턴스를 담아둔 필드

file - 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 필드

 

애노테이션을 사용하는 고전적인 예는 객체 직렬화가 있다. 직렬화는 객체를 저장장치에 저장, 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것이다. 반대인 역직렬화는 텍스트, 이진형식의 데이터로부터 원래 객체를 만들어낸다. 특히 JSON형식이 많이 쓰이며 자주쓰이는 library로 Jackson과 GSON이 있다. 

 

애노테이션 예제들은 jkid의 구현에 관한 것들이며 아래의 깃허브 주소에 있다.

https://github.com/yole/jkid/tree/master/src/test/kotlin/examples

 

GitHub - yole/jkid: JSON serialization/deserialization library for Kotlin data classes

JSON serialization/deserialization library for Kotlin data classes - GitHub - yole/jkid: JSON serialization/deserialization library for Kotlin data classes

github.com

 

애노테이션 선언은 class 앞에 annotation이라는 변경자가 붙는다.

annotation class JsonExclude

파라미터가 있는 애노테이션을 정의하려면 주 생성자에 파라미터를 선언해야 한다. 모든 파라미터 앞에 val를 붙여야한다.

annotation class JsonName(val name: String)

 

메타애노테이션은 애노테이션 클래스에 적용할 수 있는 애노테이션이다. jkid의 JsonExclude, JsonName에는 @Target 애노테이션이 붙어있다.

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

@Target(AnnotationTarget.PROPERTY)
annotation class JsonName(val name: String)

애노테이션이 붙을 수 있는 대상이 정의된 enum은 AnnotaionTarget이다. 클래스, 매서드, 프로퍼티, 애노테이션 등이 있으며 한번에 다수의 대상을 지정할 수 있다. ANNOTATION_CLASS를 대상으로 지정하면 메타애노테이션으로 만들 수 있다.

 

어떤 클래스를 선언 메타데이터로 참조할 수 있는 기능이 필요할 때 클래스 참조를 파라미터로 하는 애노테이션 클래스를 선언하면 이런 기능을 사용할 수 있다. jkid의 @DeserializeInterface 가 그 예이다.

interface Company {
    val name: String
}

data class CompanyImpl(override val name: String) : Company

data class Person(
        val name: String,
        @DeserializeInterface(CompanyImpl::class) val company: Company
)

역직렬화를 사용할 클래스를 지정하기 위해 @DeserializeInterface 애노테이션의 인자로 CopayImpl::class 를 넘긴다. 이러한 애노테이션은 다음과 같이 정의한다.

@Target(AnnotationTarget.PROPERTY)
annotation class DeserializeInterface(val targetClass: KClass<out Any>)

KClass는 자바의 java.lang.Class같은 역할을 해서 클래스 참조를 저장할 때 사용한다. 위의 코드에서 KClass<CompanyImpl>는 KClass<out Any>의 하위 타입이다. 여기서 out 키워드가 없다면 Any::class 타입만 넘길 수 있다. out 키워드가 있어서 Any를 확장하는 CompanyImpl::class 타입을 넘길 수 있게 된다.

 

애노테이션 파라미터로 제네릭 클래스를 받게 할 수도 있다. 

@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

기본적으로 ValueSerializer의 타입 인자를 지정해야하지만 어떤 타입이 쓰일 지 알 수 없어 스타 프로젝션을 사용할 수 있다. KClass<out ValueSerializer<*>>에서 out 키워드가 있으므로 ValueSerializer 이외에도 구현하는 모든 클래스를 받으며 스타 프로젝션에 의해 어떤 타입이든 직렬화 할 수 있게 허용하고 구현하지 않는 클래스는 금지한다.

 

리플렉션은 실행 시점에 동적으로 프로퍼티와 메서드에 접근할 수 있게 해주는 방법이다. 실행 시점이 되어서야만 프로퍼티나 메서드의 이름을 알 수 있는 경우가 있는데 이런 경우에 리플렉션이 필요하다.

코틀린은 자바의 리플렉션API인 java.lang.reflect 패키지를 통해 제공되는 표준 리플렉션과 코틀린의 kotlin.reflect 패키지를 통해 제공하는 리플렉션 API를 다루어야 한다. 후자는 자바에는 없는 프로퍼티나 null가능한 타입 같은 코틀린 고유 개념에 대한 리플렉션을 제공한다. 다만 자바 리플렉션을 대안으로 사용해야 하는 경우가 있다.

 

코틀린 리플렉션 API : KClass, KCallable, KFunction KProperty

KClass는 클래스를 표현하는 리플렉션으로 java.lang.Class에 해당하는 KClass를 사용하면 클래스 안의 모든 선언을 열거하고 각 선언에 접근하거나 상위 클래스를 얻는 등의 작업이 가능해진다.

val person = Person("Alice", 29)
val kClass = person.javaClass.kotlin
println(kClass.simpleName)