어노테이션과 리플렉션
## 애노테이션(Annotation) 적용하기
코틀린은 자바와 같은 방법으로 애노테이션을 사용할 수 있다.
애노테이션은 @과 이름으로 이뤄지며 함수나 클래스 등 여러 다른 코드 구성 요소에 붙일 수 있다.

> 자바라이브러리 중 일부 어노테이션에 대해서는
> 일부러 어노테이션 프로세싱에 미포함 하는 경우가 있음
https://kotlinlang.org/docs/lombok.html#using-with-kapt
코틀린 지향 > 멀티플랫폼 > 리엑트 etc...
좀 더 경량화된 symbol processor
자바 100 호환에 대해서는 trade off 를 감수하고 나온 거라서 기술검토가 확실해야함
https://kotlinlang.org/docs/ksp-overview.html#how-ksp-looks-at-source-files
코틀린에서 자주 사용되는 annotation
https://codechacha.com/ko/kotlin-annotations/
https://kotlinlang.org/docs/annotations.html
https://kotlinlang.org/docs/reflection.html
```kotlin
@Test
fun testTrue() {
Assert.assertTrue(true)
}
```
@Deprecated 애노테이션은 자바와 같은 의미이지만 코틀린에서는 replaceWith 파라미터를 사용해 대체할 수 있는 패턴을 제시할 수 있다.
```kotlin=
// ReplaceWith 어노테이션을 참조할 때는 @를 붙이지 않음
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int)
```
`Intellij IDEA`에서는 Kotlin의 @Deprecated 경고에 대해 자동으로 경고 메시지 및 Quick Fix 기능을 제공한다.
애노테이션의 인자로는 원시 타입 값, 문자열, enum, 클래스 참조, 다른 애노테이션, 배열을 넣을 수 있다.
애노테이션 인자를 지정하는 문법 <-> 자바
- 클래스를 애노테이션 인자로 지정할 때는 클래스 이름 뒤에 ::class를 넣어야 한다.
- 다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션 앞에 @를 붙이지 않는다(@Deprecated 참조).
- 배열을 인자로 지정하려면 arrayOf('a', 'b', 'c')와 같이 사용한다. 다만 자바에서 선언한 애노테이션 클래스를 사용할 때는 가변 길이 인자 형식을 취한다.
- 애노테이션 인자는 컴파일 시점에 알 수 있어야 한다.
- 프로퍼티를 애노테이션 인자로 지정하려면 const변경자를 사용해 상수로 취급해야 한다.
## 애노테이션 대상
코틀린은 소스코드에서 한 선언을 컴파일한 결과가 여러 자바 선언과 대응되는 경우가 있다.
여러 자바 선언에 각각 다른 애노테이션을 붙여야 할 때가 있을 것이다.
코틀린은 애노테이션을 붙일 때 어느 요소에 붙일 지 정할 수 있다.
사용 지점 대상(Use-site target)을 사용해서 애노테이션을 붙일 요소를 정할 수 있다.
```kotlin
@get:Rule
//@(사용 지점 대상):(애노테이션 이름)
```
#### 사용 지점 대상을 지정할 때 사용하는 대상 목록
- property: 프로퍼티 전체. 자바에서 선언된 애노테이션에는 사용할 수 없다.
- field: 프로퍼티에 의해 생성되는 필드(뒷받침하는 필드)
- get: 프로퍼티 게터
- set: 프로퍼티 세터
- receiver: 확장 함수나 프로퍼티의 수신 객체 파라미터
- param: 생성자 파라미터
- setparam: 세터 파라미터
- delegate: 위임 프로퍼티의 위임 인스턴스를 담아둔 필드
- file: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아둔 필드
- file을 사용하는 애노테이션은 package선언 앞에서 파일의 최상위 수준에만 적용할 수 있다.
```kotlin
@file:JvmName("StringFunctions")
package string
```
#### 애노테이션 선언
애노테이션을 선언하려면 클래스 앞에 annotation 키워드를 붙인다.
```kotlin
annotation class JsonExclude
```
애노테이션 클래스는 오직 선언이나 식과 관련 있는 메타데이터의 구조를 정의하기 때문에 내부에 코드가 들어있을 수 없다.
파라미터가 있는 애노테이션을 정의하려면 애노테이션 클래스의 주 생성자에 파라미터를 선언한다.
```kotlin
annotation class JsonName(val name: String)
```
일반 클래스와 다른점은 모든 파라미터 앞에 val을 붙여야 한다.
같은 코드를 자바 애노테이션으로 작성하면 다음과 같다.
```kotlin
public @interface JsonName {
String value();
}
```
자바는 어떤 애노테이션을 적용할 때 value를 제외한 모든 속성에는 이름을 명시해야 한다.
코틀린은 애노테이션 적용에 일반적인 생성자 호출을 사용한다.
따라서 이름을 붙일 수도 있고 생략할 수도 있다.
##### 어노테이션 파라이터 데이터 타입
- Java 기준 primitive type
- String
- Enum
- KClass
- 위의 언급한 선언 가능한 타입의 Array
#### 메타애노테이션
애노테이션 클래스에 적용할 수 있는 애노테이션을 메타애노테이션(Meta-annotation)이라고 한다.
표준 라이브러리에는 몇 가지 메타애노테이션이 있으며 그런 메타애노테이션들은 컴파일러가 애노테이션을 처리하는 방법을 제어한다(애노테이션을 적용할 수 있는 요소의 유형을 지정할 수 있는 @Target 등).
애노테이션 파라미터로 클래스 사용하기
클래스 참조를 파라미터로 하는 애노테이션 클래스를 선언하려면 다음과 같이 한다.
```kotlin
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
```
Kclass는 java.lang.Class 타입과 같은 역할을 하는 코틀린 타입이다.
KClass의 타입 파라미터는 KClass 인스턴스가 가리키는 코틀린 타입을 지정한다.
예를 들어 CompanyImpl::class의 타입은 KClass<CompanyImpl>이며 KClass<out Any>의 하위 타입이다(공변성).
```java=
package ch10;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 메타어노테이션
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface CustomAnnotation {
String value() default "*";
String name();
}
```
```kotlin=
annotation class JsonName(val name: String)
```
//
```java=
package ch10;
import java.lang.reflect.Field;
import java.util.Arrays;
@CustomAnnotation(name = "CustomAnnotationTest")
public class CustomAnnotationTest {
@CustomAnnotation(value = "value1", name = "name1")
private static final String str = "str";
public static void main(String[] args) {
try {
Field field = CustomAnnotationTest.class.getDeclaredField("str");
System.out.println(field.getDeclaredAnnotation(CustomAnnotation.class));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
System.out.println(CustomAnnotationTest.class.getDeclaredAnnotation(CustomAnnotation.class));
Arrays.stream(CustomAnnotationTest.class.getDeclaredAnnotations()).forEach(System.out::println);
}
}
```
#### 자바 API를 애노테이션으로 제어
https://codechacha.com/ko/kotlin-annotations/
##### @JvmName
@JvmName은 코틀린을 바이트코드로 변환할 때 JVM 시그니쳐를 변경할 때 사용합니다. 즉, 자바에서 호출되는 코틀린 함수의 이름이 변경된다는 의미입니다.
다음 코드를 보시면 @JvmName을 왜 사용하는지 알 수 있습니다.
```kotlin
// compile error
fun foo(a : List<String>) {
println("foo(a : List<String>)")
}
fun foo(a : List<Int>) {
println("foo(a : List<Int>)")
}
```
위의 두개의 foo 함수는 바이트코드로 변경될 때 시그니쳐가 동일합니다. 인자가 List<>이기 때문입니다. 컴파일하려고 하면 다음과 같은 에러가 발생합니다. List의 Generic은 구별되지 않기 때문에 두개의 Signature는 동일합니다.
```kotlin
Error:(7, 1) Kotlin: Platform declaration clash: The following declarations have the same JVM signature (foo(Ljava/util/List;)V):
fun foo(a: List<Int>): Unit defined in foo.main.kotlin in file kotlin.kt
fun foo(a: List<String>): Unit defined in foo.main.kotlin in file kotlin.kt
```
@JvmName을 사용하면 Signature를 변경할 수 있습니다. 다음 코드는 문제없이 컴파일이 됩니다.
```kotlin
@JvmName("fooListString")
fun foo(a : List<String>) {
println("foo(a : List<String>)")
}
@JvmName("fooListInt")
fun foo(a : List<Int>) {
println("foo(a : List<Int>)")
}
```
위 코틀린 코드를 자바로 변환해서 보면 다음과 같습니다. static 함수로 선언된 것은 Top level의 함수로 정의되었기 때문입니다.
```java
public static final void fooListString(@NotNull List a) {
String var1 = "foo(a : List<String>)";
System.out.println(var1);
}
public static final void fooListInt(@NotNull List a) {
String var1 = "foo(a : List<Int>)";
System.out.println(var1);
}
```
위 코드를 코틀린에서 사용할 때는 아래처럼 꼭 코틀린에서 정의한 이름을 사용해야 합니다. 코틀린이 내부적으로 알아서 올바른 것을 연결해 줍니다.
```kotlin
val listString = listOf("foo", "bar")
foo(listString)
val listInt = listOf(0, 1, 2)
foo(listInt)
```
하지만 @JvmName은 바이트 코드의 시그니쳐를 변경하기 때문에, Java에서 호출할 때는 변경된 이름으로 호출해야 합니다. 다음은 자바에서 코틀린의 함수를 호출하는 코드입니다. 함수 이름 앞에 KotlinKt가 붙은 것은 kotlin.kt 파일의 Top level에 함수가 정의되었기 때문입니다.
```java
List<String> listString = new ArrayList<>();
KotlinKt.fooListString(listString);
List<Integer> listInt = new ArrayList<>();
KotlinKt.fooListInt(listInt);
```
정리하면, 함수 이름과 인자, 리턴 타입이 동일하기 때문에 컴파일 안되는 경우가 있습니다. @JvmName을 사용하면 시그니쳐를 변경하여 컴파일되도록 할 수 있습니다.
주의할 점은 코틀린은 코틀린에서 정의된 함수 이름을 사용해야 합니다. 반면에 자바는 변경된 이름으로 호출해야 합니다.
##### @JvmStatic
@JvmStatic은 static 변수의 get/set 함수를 자동으로 만들라는 의미입니다. 예제로 알아보겠습니다.
다음 Bar 클래스는 barSize라는 변수를 companion object에 선언함으로써, 전역변수를 만들었습니다.
```kotlin
class Bar {
companion object {
var barSize : Int = 0
}
}
```
사실 companion object는 자바의 static과 다릅니다.
자바로 변환해보면 Bar클래스에 barSize는 선언되었지만 get/set함수는 Bar.Companion 클래스에 등록되었습니다.
```java
public final class Bar {
private static int barSize;
public static final class Companion {
public final int getBarSize() {
return Bar.barSize;
}
public final void setBarSize(int var1) {
Bar.barSize = var1;
}
}
}
```
자바에서 get/set 함수에 접근하려면 다음처럼 Companion을 꼭 써줘야 합니다.
```kotlin
Bar.Companion.getBarSize();
Bar.Companion.setBarSize(10);
```
@JvmStatic는 Bar 바로 밑에 get/set함수가 생성되게끔 만듭니다.
아래 코틀린 코드는 barSize를 선언할 때 @JvmStatic와 함께 선언했습니다.
```kotlin
class Bar {
companion object {
@JvmStatic var barSize : Int = 0
}
}
```
위 코드를 자바로 변환해보면, 다음과 같습니다.
Bar 바로 밑에 get/set함수가 생겼고, Companion은 이전과 동일하게 get/set을 같고 있습니다.
```java
public final class Bar {
private static int barSize;
public static final int getBarSize() {
return barSize;
}
public static final void setBarSize(int var0) {
barSize = var0;
}
public static final class Companion {
public final int getBarSize() {
return Bar.barSize;
}
public final void setBarSize(int var1) {
Bar.barSize = var1;
}
}
}
```
자바에서 위 코드를 접근하면 Bar.Companion 도 가능하지만 Bar.getBarSize 처럼 바로 접근도 됩니다.
```java
Bar.getBarSize();
Bar.setBarSize(10);
Bar.Companion.getBarSize();
Bar.Companion.setBarSize(10);
```
정리하면, @JvmStatic는 Companion에 등록된 변수를 자바의 static처럼 선언하기 위한 annotation입니다.
##### @JvmField
@JvmField는 get/set을 생성하지 말라는 의미입니다.
다음 코틀린 코드에서 프로퍼티 var barSize는 get/set함수를 생성합니다.
```kotlin
class Bar {
var barSize = 0
}
```
자바로 변환해보면 다음처럼 get/set 함수가 생성되었습니다.
```java
public final class Bar {
private int barSize;
public final int getBarSize() {
return this.barSize;
}
public final void setBarSize(int var1) {
this.barSize = var1;
}
}
```
이번엔 위와 동일한 코드에서 프로퍼티에 @JvmField를 붙였습니다.
```kotlin
class Bar {
@JvmField
var barSize = 0
}
```
자바로 변환해보면 get/set 함수가 생성되지 않은 것을 볼 수 있습니다.
```java
public final class Bar {
@JvmField
public int barSize;
}
```
##### @Throws
@Throws는 코틀린 함수가 예외를 던질 수 있다는 것을 표시합니다. 사실 코틀린에는 자바의 throws와 같은 코드가 없습니다.
아래는 자바코드이고, 함수에서 NumberFormatException 예외를 던질 수 있다고 명시되어있습니다. 이 함수를 사용하려면 try-catch로NumberFormatException에 대한 예외처리를 해야 합니다.
```kotlin
void convertStringToInt(String str) throws NumberFormatException {
...
}
```
코틀린에서 위와 같이 함수에 예외를 던진다고 명시하고 싶으면, @Throws를 사용하면 됩니다.
아래 코드처럼 @Throws에 인자로 NumberFormatException::class와 같은 형태로 예외를 명시해주면 됩니다.
```kotlin
@Throws(NumberFormatException::class)
fun convertStringToInt(str: String) {
....
}
```
위 코드를 자바로 변환해보면, 아래처럼 throws NumberFormatException이 함께 함수가 정의됩니다.
```java
public static final void convertStringToInt(@NotNull String str) throws NumberFormatException {
....
}
```
##### @JvmOverloads
@JvmOverloads는 코틀린 함수의 오버로딩 메소드들을 생성해주는 annotation입니다.
예를들어, 아래 코드는 생성자의 인자가 3개이지만, 2개는 기본인자(default arguments)로 선언되어있습니다.
```kotlin
class Bar(var name: String, var size: Int = 0, var max: Int = 0) {
init {
println("Bar init: $name, $size, $max")
}
}
```
따라서, 코틀린에서 아래처럼 Bar 객체를 생성할 수 있습니다. 인자를 써주지 않으면 기본으로 설정된 값이 들어갑니다.
```kotlin
Bar("aa")
Bar("aa", 10)
Bar("aa", 10, 11)
```
하지만 위의 코틀린 클래스를 자바로 변환하면 다음처럼 3개의 인자를 갖고 있는 생성자만 생성됩니다. 그 이유는 자바는 기본인자 개념이 없기 때문입니다.
```java
public final class Bar {
private String name;
private int size;
private int max;
public Bar(String name, int size, int max) {
String var4 = "Bar init: " + this.name + ", " + this.size + ", " + this.max;
System.out.println(var4);
}
```
그래서 자바에서 위의 클래스를 생성하려면 아래처럼 모든 인자를 입력해줘야 합니다.
```java
new Bar("aa", 10, 11);
```
그렇지 않으면 아래 코드처럼 코틀린에서 오버로딩 메소드를 만들어줘야 합니다.
```java
class Bar (var name: String, var size: Int = 0, var max: Int = 0) {
constructor(name: String, size: Int): this(name, size, 0)
constructor(name: String): this(name, 0, 0)
init {
println("Bar init: $name, $size, $max")
}
}
```
@JvmOverloads은 위의 오버로딩 메소드를 자동으로 생성해주는 annotation입니다. 이것을 함수 앞에 붙여주면, 바로 위에서 오버로딩 메소드를 만든 것처럼 코드를 자동 생성해줍니다.
다음은 위의 코틀린 클래스에 이 annotation을 붙여 오버로딩 메소드를 생성한 코드입니다.
```kotlin
class Bar
@JvmOverloads constructor(
var name: String, var size: Int = 0, var max: Int = 0) {
init {
println("Bar init: $name, $size, $max")
}
}
```
자바로 변환해서 보면 아래 처럼 오버로딩 메소드들이 자동으로 생성된 것을 볼 수 있습니다. 인자가 4개짜리인 생성자가 있는데, 내부적으로 이런식으로 구현되었을 뿐이고 자바에서는 이 생성자는 드러나지 않습니다.
```java
public final class Bar {
@NotNull
private String name;
private int size;
private int max;
@JvmOverloads
public Bar(@NotNull String name, int size, int max) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.size = size;
this.max = max;
String var4 = "Bar init: " + this.name + ", " + this.size + ", " + this.max;
System.out.println(var4);
}
@JvmOverloads
public Bar(@NotNull String name, int size) {
this(name, size, 0, 4, (DefaultConstructorMarker)null);
}
@JvmOverloads
public Bar(@NotNull String name) {
this(name, 0, 0, 6, (DefaultConstructorMarker)null);
}
@JvmOverloads
public Bar(String var1, int var2, int var3, int var4, DefaultConstructorMarker var5) {
....
this(var1, var2, var3);
}
}
```
정리하면, @JvmOverloads는 오버로딩 메소드를 자동 생성해주는 annotation입니다. 코틀린 코드만 사용하면 사용할 필요가 없는 annotation입니다. 자바에서 코틀린 코드를 사용할 때 코틀린의 기본인자 개념을 적용하기 위해 사용되는 것입니다.
리플렉션 : 실행 시점에 코틀린 객체 내부 관찰
* 리플렉션(Reflection)은 런타임 프로그램의 구조(객체, 함수, 프로퍼티, 생성자, Bonud)를 분석해서 활용할 수 있는 기법이다. 이를 활용하면 런타임에 얻을 수 있는 정보를 기반으로 기존 코드 대비, 간결한 구조의 표현이 가능해진다.
* 타입과 관계 없이 객체를 다뤄야 하거나 객체가 제공하는 메소드나 프로퍼티 이름을 오직 실행 시점에만 알 수 있는 경우가 존재한다.
* JSON 직렬화 라이브러리가 바로 그 예이다. 직렬화 라이브러리는 어떤 객체든 JSON으로 변환할 수 있어야 하고, 실행 시점에 되기 전까지는 라이브러리가 직렬화할 프로퍼티나 클래스에 대한 정보를 알 수 없다. 이런 경우 리플렉션을 사용해야 한다.
* 자바에서 제공하는 ```java.lang.reflect``` 패키지이다. 자바 리플렉션 API가 필요한 이유는 코틀린 클래스는 일반 자바 바이트 코드로 컴파일되기 때문이다. 자바 리플렉션 API는 코틀린 클래스를 컴파일한 바이트코드도 완벽히 지원한다.
* 코틀린의 kotlin.reflect 에서 제공하는 API이다. 이 API는 자바에서는 없는 프로퍼티나 널이 될 수 있는 타입과 같은 코틀린 고유 개념에 대한 리플렉션을 제공한다.
KClass
* KClass API 는 java.lang.Class 와 동일한 역할을 한다.
* Person::class 라는 식을 사용하면 KClass 인스턴스를 얻을 수 있다.
* 실행 시점에 객체의 클래스를 얻으려면, 객체의 kClass 프로퍼티를 사용해, 자바클래스를 얻어야 하는데 이는 java.lang.Object.getClass() 와 동일하다.
* 자바클래스를 얻어 .kotlin 확장 프로퍼티를 통해 코틀린 리플렉션 API 로 변환할 수 있다.
```kotlin
fun main() {
val person = Person(“ncucu”, 27)
val kClass = person.javaClass.kotlin
println(kClass.simpleName)
kClass.memberProperties.forEach { println(it.name) }
}
`KClass Interface`
* 모든 멤버의 목록이 KCallable 인스턴스의 컬렉션이다.
* KCallable 은 함수와 프로퍼티를 아우르는 공통 상위 인터페이스 이며 내부에는 call 메소드가 있다. 이를 사용하면 함수 혹은 프로퍼티의 게터를 호출할 수 있다.
public actual interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier {
public actual val simpleName: String?
public actual val qualifiedName: String?
override val members: Collection<KCallable<*>>
public val constructors: Collection<KFunction<T>>
public val nestedClasses: Collection<KClass<*>>
…
}
KCallable Interface
public actual interface KCallable<out R> : KAnnotatedElement {
public fun call(vararg args: Any?): R
…
}
```
`Kotlin Class 함수 호출의 예제`
call 메소드는 모든 타입의 함수에 적용할 수 있는 메소드지만, 타입 안전성을 보장해 주지 않는다.
KFunction 의 인자와 반환 타입을 모두 안다면 invoke 메소드를 호출할 것을 권장한다.
```kotlin
fun foo(x: Int) = println(x)
/**
* 코틀린 리플렉션 API 를 활용한 함수호출 예제
*/
fun main() {
// ::foo 로 함수참조를 사용해 KFunction 클래스의 인스턴스를 받아온다.
val kFunction = ::foo
// KCallable 인터페이스의 call 메소드를 호출해서 해당 함수를 호출한다.
kFunction.call(1)
// KFunction 인터페이스의 invoke 메소드는 좀 더 구체적인 정보를 담고 있다.
kFunction.invoke(1)
}
```
`KFunctionN 인터페이스`
* KFunction1 과 같은 타입은 파라미터 개수가 다른 여러 함수를 표현한다.
* KFunctionN 은 KFunction 을 확장한다.
* 이런 함수 타입들은 컴파일러가 생성한 합성 타입 (synthetic compiler-generated type) 이다.
* 코틀린에서는 컴파일러가 생성한 합성 타입을 사용하기 때문에 원하는 수 만큼 많은 파라미터를 가지는 함수 인터페이스를 사용할 수 있다.
```kotlin
/**
* KProperty 를 이용하면 프로퍼티를 참조할 수 있다.
* KFunction 과 마찬가지로 call 메소드를 사용할 수 있지만, set, get 과 같은 더 안전한 방법을 제공한다.
*/
var counter = 0
fun main() {
val kProperty = ::counter
kProperty.setter.call(1)
kProperty.set(1)
kProperty.getter.call()
kProperty.get()
}
```
kProperty
* KProperty는 call 메소드를 호출할 수 있고, get 메소드 또한 지원한다.
* 최상위 수준이나 클래스 안에 정의된 프로퍼티만 리플렉션으로 가져올 수 있고 함수의 로컬 변수에는 접근할 수 없다.
```kotlin
var counter = 0
val kProperty = ::counter
kProperty.setter.call(21)
println(kProperty.get())
// 21
```
### Class Reflection
https://ence2.github.io/2021/03/%EC%BD%94%ED%8B%80%EB%A6%B0-%EB%A6%AC%EC%84%9C%EC%B9%98-reflection8/
```kotlin
import kotlin.reflect.KClass
data class Person(var name:String, var age:Int)
open class Base(x:Int)
class Derived(x:Int) : Base(x)
fun process(b:Base){
if (b is Derived) // Smart cast
{
println(b::class.qualifiedName)
}
}
fun main(args: Array<String>)
{
// kotlin의 refection
var c: KClass<Person> = Person::class // reflection 대상 객체
println(c.qualifiedName) // -> kotlinMySample.Person
println(c.members.map{it.name}) // -> [age, name, component1, component2, copy, equals, hashCode, toString]
println(“Is it a companion? ${c.isCompanion}”) // -> false
var z:Base = Derived(10)
process(z) // 부모 변수로 자식 객체의 이름을 얻어 올 수 있음 -> kotlinMySample.Person
// java의 refelction
var j = c.java
println(j.simpleName)
}
```
어노테이션을 활용한 JSON 직렬화 제어
* 직렬화 : 객체를 저장장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것.
* 역직렬화 : 텍스트나 이진 형식으로 저장된 데이터로부터 원래의 객체를 만들어낸다.
* 직렬화에 자주 쓰이는 형식은 JSON이며, JSON을 변환할 때 자주 쓰이는 라이브러리는 잭슨과 지슨이 있다.
* jkid 라이브러리를 사용한다.
* ```serialize``` : 이 함수를 사용해 객체를 직렬화 하여 JSON 표현이 담긴 문자열을 얻는다.
* ```deserialize``` : 이 함수를 사용해 JSON 표현을 객체로 만들 수 있으며, 원하는 객체의 타입을 지정해야 한다.
* ```jkid 라이브러리```에서는 어노테이션을 활용해 객체를 직렬화하거나 역직렬화하는 방법을 제어할 수 있다.
* 객체 -> JSON으로 직렬화할 때, jkid는 기본적으로 모든 프로퍼티를 직렬화하며, 프로퍼티 이름을 키로 사용한다.
* 어노테이션을 활용해 이 동작을 바꿔보자.
* @JsonExclude : 어노테이션을 사용하면 직렬화나 역직렬화시 그 프로퍼티를 무시할 수 있다.
* @JsonName : 어노테이션을 사용하면 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 어노테이션이 지정한 이름을 쓰게 할 수 있다.
```kotlin
data class Person{
@JsonName(“alias”) val firstName: String,
@JsonExclude val age: Int?=null
}
```
firstName 프로퍼티를 JSON으로 저장할 때 사용하는 키를 alias로 한다.
* age 프로퍼티는 직렬화나 역직렬화시 사용되지 않는다. 이처럼 직렬화 대상에서 제외할 프로퍼티에는 반드시 디폴트 값을 지정해야만 한다. 그렇지 않으면 역직렬화시 Person의 인스턴스를 새로 만들 수 없다.
어노테이션 선언
* 어노테이션 클래스는 오직 선언이나 식과 관련있는 메타 데이터의 구조를 정의하기 때문에 내부에 아무 코드도 들어있을 수 없다.
* 이러한 이유로 컴파일러는 어노테이션 클래스에서 본문을 정의하지 못하게 막는다. 파라미터가 있는 어노테이션을 정의하려면 어노테이션 클래스의 주 생성자에 파라미터를 선언해야 한다.
* 일반 클래스의 주 생성자 선언과 같지만, 모든 파라미터 앞에 val를 붙여줘야 한다.
```kotlin
annotation class JsonExclude
```
```kotlin
annotation class JsonName(val name: String)
```
* Java에서는 이렇게 쓴다.
```kotlin
public @interface JsonName {
String value();
}
```
메타 어노테이션을 지정한다.
```kotlin
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
```
* 대상을 PROPERTY로 지정한 어노테이션은 자바에서 사용할 수 없다. 자바에서 사용하려면 AnnotationTarget.FIELD를 두번째 대상으로 추가해줘야 한다.
* 그렇게 하면 어노테이션을 코틀린 프로퍼티와 자바 필드에 적용할 수 있다.
```kotlin
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation
@BindingAnnotation
annotation class MyBinding
```
* 클래스 참조를 파라미터로 하는 어노테이션 클래스를 선언하면 어떤 클래스를 선언 메타 데이터로 참조할 수 있다.
* e.g.) jkid의 @DeserializeInterface -> 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 쓰는 어노테이션이다.
* 인터페이스의 인스턴스를 직접 만들 수 없다. 따라서 역직렬화 시 어떤 클래스를 사용해 인터페이스를 구현할 지를 지정할 수 있어야 한다.
```kotlin
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
}
```
* 직렬화된 Person 인터페이스를 역직렬화하는 과정에서 company 프로퍼티를 표현하는 JSON을 읽으면 jkid는 그 프로퍼티 값에 해당하는 JSON을 역직렬화하면서 CompanyImpl의 인스턴스를 만들어서 Person 인스턴스의 company 프로퍼티에 설정한다.
이렇게 역직렬화를 사용할 클래스를 지정하기 위해 @DeserializeInterface 어노테이션의 인자로 CompanyImpl::class를 넘긴다.
* KClass의 타입 파라미터는 이 KClass의 인스턴스가 가리키는 코틀린 타입을 지정한다.
CompanyImpl::class의 타입은 KClass< CompanyImpl >이며, 이 타입은 위에서 살펴본 DeserializeInterface의 파라미터 타입인 KClass< out Any >의 하위 타입이다.
* KClass의 타입 파라미터를 쓸 때, out 없이 KClass< Any > 라고 쓰면 DeserializeInterface에게 CompanyImpl::class를 인자로 넘길 수 없고, 오직 Any::class만 넘길 수 있다.
* 반면, out이 존재하면 모든 코틀린 타입 T에 대해 KClass< T >가 KClass< out Any >의 하위 타입이 된다. 이는 9장 제네릭에서 살펴본 공변성 개념이다.
따라서 DeserializeInterface의 인자로 Any 뿐 아니라 Any를 확장하는 모든 클래스에 대한 참조를 전달할 수 있다.
```kotlin
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
```
리플렉션을 활용한 객체 직렬화 구현
```kotlin
private fun StringBuilder.serializeObject(obj: Any) {
val kClass = obj.javaClass.kotlin // 객체의 KClas를 얻는다.
val properties = kClass.memberProperties // 클래스의 모든 프로퍼티를 얻는다.
properties.joinToString(this, prefix = “{“, postfix = “}”) { prop ->
serializeString(prop.name) // 프로퍼티 이름을 얻는다.
append(“: “)
serializePropertyValue(prop.get(obj)) // 프로퍼티 값을 얻는다.
}
}
```