리플렉션 : 실행 시점에 객체의 프로퍼티와 메서드에 접근할 수 있게 해주는 방법.
코틀린에서의 리플렉션
1) 자바가 java.lang.reflect 패키지를 통해 제공하는 표준 리플렉션
2) 코틀린이 kotlin.reflect 패키지를 통해 제공하는 코틀린 리플렉션 API = 자바에는 없는 프로퍼티나, 널이 될 수 있는 타입과 같은 코틀린 고유 개념에 대한 리플렉션을 제공한다.(아직까지는 자바 리플렉션 API를 완벽 대체할 수 있지는 않다)
KClass, KCallable, KFunction, KProperty(코틀린 리플렉션)
java.lang.Class에 해당하는 KClass를 사용하면 클래스 안에 있는 모든 선언을 열거하고, 각 선언에 접근하거나, 클래스의 상위 클래스를 얻는 등의 작업이 가능하다.(MyClass::class 식을 사용해 KClass의 인스턴스를 얻을 수 있다)
실행 시점에 객체의 클래스를 얻으려면, 먼저 객체의 javaClass 프로퍼티를 사용해 객체의 자바 클래스를 얻어야 한다(javaClass == java.lang.object.getClass()). 클래스를 얻었다면,. kotlin 확장 프로퍼티를 통해 자바에서 코틀린 리플렉션 API로 옮겨올 수 있다.
class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val person = Person("Alice", 29)
val kClass = person.javaClass.kotlin //KClass<Person>의 인스턴스를 반환한다.
println(kClass.simpleName)
kClass.memberProperties.forEach {
println(it.name)
}
}
- 클래스 이름과, 그 클래스에 들어있는 프로퍼티 이름을 출력하고, memberProperties를 통해 클래스와, 모든 조상 클래스 내부에 정의된 비확장 프로퍼티를 모두 가져온다.
public actual interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier {
/**
* The simple name of the class as it was declared in the source code,
* or `null` if the class has no name (if, for example, it is a class of an anonymous object).
*/
public actual val simpleName: String?
/**
* The fully qualified dot-separated name of the class,
* or `null` if the class is local or a class of an anonymous object.
*/
public actual val qualifiedName: String?
/**
* All functions and properties accessible in this class, including those declared in this class
* and all of its superclasses. Does not include constructors.
*/
override val members: Collection<KCallable<*>>
/**
* All constructors declared in this class.
*/
public val constructors: Collection<KFunction<T>>
/**
* All classes declared inside this class. This includes both inner and static nested classes.
*/
public val nestedClasses: Collection<KClass<*>>
/**
* The instance of the object declaration, or `null` if this class is not an object declaration.
*/
public val objectInstance: T?
//...
}
- KClass에 제공하는 메서드이다.
- kotlin-reflect 라이브러리를 통해 KClass에 대해 많은 확장 함수 메서드를 제공한다.
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/
KClass - Kotlin Programming Language
kotlinlang.org
public actual interface KCallable<out R> : KAnnotatedElement {
public actual val name: String
public val parameters: List<KParameter>
public val returnType: KType
public fun call(vararg args: Any?): R
public fun callBy(args: Map<KParameter, Any?>): R
//...
}
fun main(args: Array<String>) {
val kFunction = ::foo
kFunction.call(42)
}
fun foo(x: Int) = println(x)
- KCallable은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스이다.(내부에 call 메서드가 있고, call을 사용하면, 함수나 프로퍼티의 게터를 호출할 수 있다)
- call을 사용할 때는 함수 인자를 vararg 리스트로 전달한다.
- ::foo 식의 값 타입은 리플렉션 API에 있는 KFunction 클래스의 인스턴스이다.
- 이 함수 참조가 가리키는 함수를 호출하려면 kCallable.call 메서드를 호출한다.
- call에 넘긴 인자 개수와 원래 함수에 정의된 파라미터 개수는 맞아떨어져야 한다.(안 맞을 경우 IllegalArgumentException 발생)
- 함수를 호출하기 위해 더 구체적인 메서드를 사용할 수도 있다.
- KFunction1<Int, Unit> 에는 파라미터와 반환 값 타입 정보가 들어 있다.(1은 함수의 파라미터가 1개라는 의미)
- KFunction1 인터페이스를 통해 함수를 호출하려면 invoke 메서드를 사용해야 한다.
- invoke는 정해진 개수의 인자만 받아들이며, 인자 타입은 첫 번째 타입 파라미터와 같다.
import kotlin.reflect.KFunction2
fun main(args: Array<String>) {
val kFunction:KFunction2<Int, Int, Int> = ::sum
println(kFunction.invoke(1, 2) + kFunction(3, 4))
//kFunction(1) //Error : No value passed for parameter p2
}
fun sum(x: Int, y: Int) = x + y
- KFunction의 invoke 메서드를 호출할 때는 인자 개수나 타입이 맞아떨어지지 않으면 컴파일이 안된다.
- 인자 타입과 반환 타입 모두 않다면 KFunctionN을 호출하는 것이 좋다
- call 메서드는 모든 타입의 함수에 적용할 수 있는 일반적인 메서드라 타입 안전성을 보장해주지는 않는다.
TIP : 언제, 어떻게 KFunctionN 인터페이스가 정의되는가?
KFunctionN 타입은 KFunction을 확장하며, N과 파라미터 개수가 같은 invoke를 추가로 포함한다. 이런 함수 타입들은 컴파일러가 생성한 합성 타입이라, kotlin.reflect 패키지에서 정의를 찾을 수 없다. 합성 타입을 사용하기 때문에 코틀린은 kotlin-runtime.jar 크기를 줄일 수 있고, 함수 파라미터 개수에 대한 인위적인 제약을 피할 수 있다.
var counter = 0
fun main(args: Array<String>) {
val kProperty = ::counter
kProperty.setter.call(21) //리플렉션 기능을 통해 세터를 호출하면서 21을 인자로 넘긴다
println(kProperty.get()) //get을 호출해 프로퍼티 값을 가져온다
}
- KProperty의 call 메서드를 호출할 수도 있다.
- KProperty의 call은 프로퍼티의 게터를 호출하지만, 프로퍼티 인터페이스는 프로퍼티 값을 얻는 더 좋은 방법으로 get 메서드 제공
- 최상위 프로퍼티는 KProperty0 인터페이스의 인스턴스로 표현되며, kProperty0 안에는 인자가 없는 get 메서드가 있다.
fun main(args: Array<String>) {
val person = Person("Alice", 29)
val memberProperty = Person::age
println(memberProperty.get(person))
}
- 멤버 프로퍼티는 KProperty1 인스턴스로 표현된다.
- 인자가 1개인 get 메서드가 들어있다.
- 멤버 프로퍼티의 값을 가져오려면 get 메서드에게 프로퍼티를 얻고자 하는 객체 인스턴스를 넘겨야 한다.
public actual interface KProperty<out V> : KCallable<V> {
/**
* `true` if this property is `lateinit`.
* See the [Kotlin language documentation](https://kotlinlang.org/docs/reference/properties.html#late-initialized-properties)
* for more information.
*/
@SinceKotlin("1.1")
public val isLateinit: Boolean
/**
* `true` if this property is `const`.
* See the [Kotlin language documentation](https://kotlinlang.org/docs/reference/properties.html#compile-time-constants)
* for more information.
*/
@SinceKotlin("1.1")
public val isConst: Boolean
/** The getter of this property, used to obtain the value of the property. */
public val getter: Getter<V>
/**
* Represents a property accessor, which is a `get` or `set` method declared alongside the property.
* See the [Kotlin language documentation](https://kotlinlang.org/docs/reference/properties.html#getters-and-setters)
* for more information.
*
* @param V the type of the property, which it is an accessor of.
*/
public interface Accessor<out V> {
/** The property which this accessor is originated from. */
public val property: KProperty<V>
}
/**
* Getter of the property is a `get` method declared alongside the property.
*/
public interface Getter<out V> : Accessor<V>, KFunction<V>
}
- KProperty1은 제네릭 클래스이다. KProperty 첫 번째 타입 파라미터는 수신 객체 타입, 두 번째 타입 파라미터는 프로퍼티 타입을 표현한다.
- 수신 객체를 넘길 때는 KProperty1의 타입 파라미터와 일치하는 타입의 객체만 넘길 수 있다.
- 최상위 수준이나 클래스 안에 정의된 프로퍼티만 리플렉션 접근할 수 있고, 함수의 로컬 변수에는 접근할 수 없다.

- KClass, KFunction, KParameter 모두 KAnnotatedElement를 확장한다.
- KClass는 클래스와 객체를 표현할 때 쓰인다.
- KProperty는 모든 프로퍼티를 표현할 수 있고, 하위 클래스인 KMutableProperty는 var로 정의한 변경 가능한 프로퍼티를 표현한다
- Getter, Setter는 모두 KFunction을 확장한다.
리플렉션을 사용한 객체 직렬화 구현
제이 키드 직렬화 함수 선언
fun serialize(obj: Any): String
private fun StringBuilder.serializeObject(x: Any){
append(x)
}
- 객체를 받아, 객체에 대한 JSON 표현을 문자열로 반환한다.
- 객체의 프로퍼티와, 값을 직렬화 하면서, StringBuilder 객체 뒤에 직렬화 한 문자열을 추가한다
- append 호출을 간결하게 수행하기 위해 직렬화 기능을 StringBuilder을 확장 함수로 구현
- serializedObject는 StringBuilder API를 확장하지 않는다.
- serializedObject가 수행하는 연산은 이 맥락을 벗어나면 쓸모없다.(private로 선언)
- 확장 함수로 만든 이유는 코드 블록에서 주로 사용하는 객체를 명시하고, 객체를 더 쉽게 다루기 위함
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
- serialize는 대부분 작업을 serializedObject에 위임
- buildString은 StringBuilder를 생성해 인자로 받은 람다에 넘긴다.
- 람다 안에서 StringBuilder 인스턴스를 this로 사용할 수 있다.
- 람다 본문에서 serializeObject(obj)를 호출해서 obj를 직렬화한 결과를 StringBuilder에 추가
serializeObject 구현
private fun StringBuilder.serializeObject(obj: Any){
val kClass = obj.javaClass.kotlin //객체의 class를 받는다
val properties = kClass.memberProperties //클래스의 모든 프로퍼티를 받는다
properties.joinToString(
this, prefix = "{", postfix = "}"){prop ->
serializeString(prop.name)//프로퍼티 이름을 얻는다
append(": ")
serializePropertyValue(prop.get(obj)) //프로퍼티 값을 얻는다
})
}
- 클래스의 각 프로퍼티를 차례로 직렬화하며, 결과는 JSON형태이다.
- joinToStringBuilder함수는 프로퍼티를 콤마(,)로 분리해주고, serializeString 함수는 JSON 명세에 따라 특수문자를 이스케이프 한다.
- serializePropertyValue 함수는 어떤 값이 원시, 문자열, 컬렉션, 중첩된 객체 인지 판단하고, 적절하게 직렬화한다.
- 어떤 객체의 클래스에 정의된 모든 프로퍼티를 열거하기 때문에 각 프로퍼티가 어떤 타입인지 알 수 없고, prop 변수의 타입은 KProperty<Any, *>이며, prop.get(obj) 메서드 호출은 Any타입 값을 반환한다.
- 수신 객체 타입을 컴파일 시점에 검사할 방법은 없지만, 어떤 프로퍼티의 get에 넘기는 객체가 바로 프로퍼티를 얻어온 객체이기 때문에 항상 프로퍼티 값이 제대로 반환된다.
애노테이션을 활용한 직렬화 제어
@JsonExclude : 어떤 프로퍼티를 직렬화에서 제외하고 싶을 때 이 애노테이션을 쓸 수 있다.
KAnnotatedElement 인터페이스에는 annotations 프로퍼티가 있다. annotations는 소스코드상에서 해당 요소에 적용된 모든 애노테이션 인스턴스의 컬렉션이다. KProperty는 KAnnotationElement를 확장하므로 property.annotations를 통해 프로퍼티의 모든 애노테이션을 얻을 수 있다. 여기서는 모든 애노테이션을 사용하지 않고, 한 애노테이션만 찾으면 되므로, findAnnotation 함수를 사용한다.

- 인자로 전달받은 타입에 해당하는 애노테이션이 있으면 그 애노테이션을 반환한다.
- 타입 파라미터를 reified로 만들어 애노테이션 클래스를 타입 인자로 전달한다.

- filter를 사용하여, @JsonExclude로 애노테이션 된 프로퍼티를 없앨 수 있다.
@JsonName : 프로퍼티를 직렬화 해서 JSON에 넣을 때 사용될 이름을 인자로 받아, 직렬화 할 때 사용될 인자 이름으로 직렬화한다.
애노테이션의 존재 여부와, 전달한 인자도 알아야 한다.

- 프로퍼티에 @JsonName 애노테이션 값이 없다면, jsonNameAnn이 널이고, prop.name을 JSON을 프로퍼티 이름으로 사용할 수 있다.
- 프로퍼티에 애노테이션이 있다면 애노테이션이 지정하는 이름을 대신 사용한다
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

- @CustomSerializer는 getSerializer라는 함수에 기초한다.
- getSerializer는 @CustomSerializer를 통해 등록한 ValueSerializer 인스턴스를 반환한다.
- getSerializer가 다루는 객체가 KProperty 인스턴스이기 때문에 KProperty 확장 함수로 정의
- getSerializer는 findAnnotation 함수를 호출해 @CustomSerializer가 있는지 찾아본다.
- 있다면 애노테이션의 SerializerClass가 직렬화기 인스턴스를 얻기 위해 사용해야 할 클래스다.
- @CustomErializer의 값으로 클래스와 객체를 처리하는 방식을 보면, 클래스와 객체 모두 KClass로 표현되지만, 객체에는 object 선언에 의해 생성된 싱글턴을 가리키는 objectInstance 프로퍼티가 있다.
- objectInstance 프로퍼티에 DateSerializer의 싱글턴 인스턴스가 들어있어 이를 사용해 모든 객체를 직렬화하면 되므로 createInstance를 호출할 필요가 없다.
- KClass가 일반 클래스를 표현한다면, createInstance를 호출해 새인 스턴스를 만들어야 한다.

- 프로퍼티에 대해 정의된 커스텀 직렬화기가 있으면 그 커스텀 직렬화기를 사용
- 커스텀 직렬화기가 없다면, 일반적인 방법을 따라 프로퍼티 직렬화
'KotlinInAction > 애노테이션과 리플렉션' 카테고리의 다른 글
| 리플렉션: 실행 시점에 코틀린 객체 내부 관찰 2 (0) | 2022.08.17 |
|---|---|
| 애노테이션 선언과 적용 (0) | 2022.08.15 |