Kotlin in Action Chapter 10

Annotation & Reflection

Sigrid Jin
31 min readMar 19, 2022
https://kotlinlang.org/

Meaning

어느 함수를 호출하려면, 함수가 정의된 클래스의 이름과 함수 이름, 파라미터의 이름을 알아야 한다.

이러한 제약에서 벗어나, 우리가 미리 알지 못하는 임의의 클래스를 arbitrary하게 다룰 수 있는 것이 Annotation과 Reflection의 특징이다.

Annotation: You can use annotations to **assign library-specific semantics** to those classes;
Reflection: Reflection allows you to **analyze the structure of the classes at runtime.

Applying annotations

import org.junit.*
class MyTest {
@Test fun testTrue() {
Assert.assertTrue(true)
}
}

Kotlin에는 enhanceWith가 있다. 클래스를 애노테이션 인자로 지정할 때는 @MyAnnotation(MyClass::class)처럼 ::class를 클래스 이름 뒤에 넣어야 한다.
다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 @를 넣지 않아야 한다. 아래의 ReplaceWith 앞에 @를 사용하지 않는다.

@Deprecated(“Use removeAt(index) instead.”, ReplaceWith(“removeAt(index)”))
fun remove(index: Int) { … }
  • 배열을 인자로 지정하려면 @RequestMapping(path=arrayOf(“/foo”, “/bar”)) 처럼 arrayOf 함수를 사용한다. 자바에서 선언한 애노테이션 클래스를 사용한다면 value라는 이름의 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환된다.
  • 따라서 그런 경우에는 @JavaAnnotationWithArrayValue(“abc”, “foo”, “bar”) 처럼 arrayOf 함수를 쓰지 않아도 된다. 애노테이션 인자를 컴파일 시점에 알 수 있어야 한다. 따라서 임의의 프로퍼티를 인자로 지정할 수는 없다.
  • 프로퍼티를 애노테이션 인자로 사용하려면 그 앞에 const 변경자를 붙여야 한다. 컴파일러는 const가 붙은 프로퍼티를 컴파일 시점 상수로 취급한다.
  • const가 붙은 프로퍼티는 파일의 맨 위나 object 안에 선언해야 하며 원시 타입이나 String으로 초기화해야 한다.
const val TEST_TIMEOUT=100L
@Test(timeout=TEST_TIMEOUT) fun testMethod() {…}

Annotation Targets

자바 또는 코틀린에 선언된 애노테이션을 사용해 프로퍼티에 애노테이션을 붙이는 경우 사용 시점 대상(use-site target) 선언으로 애노테이션을 붙일 요소를 정할 수 있다. 만약 getter에 어노테이션을 붙이고 싶다면 다음과 같이 선언할 수 있다.

@get:MyAnnotation
val temp = Temp()
  • 자바에 선언된 애노테이션을 사용해 프로퍼티에 애노테이션을 붙이는 경우, 위에서 살펴본 바와 같이 기본적으로 프로퍼티의 필드에 그 애노테이션을 붙인다. 하지만 코틀린으로 애노테이션을 선언하면 프로퍼티에 직접 적용할 수 있는 애노테이션을 만들 수 있다.
  • @Retention은 정의 중인 어노테이션 클래스를 소스 수준에서만 유지할지, .class 파일에 저장할지, 실행 시점에 리플렉션을 사용해 접근할 수 있게 할지를 지정하는 메타 어노테이션이다.
  • 자바 컴파일러는 기본적으로 어노테이션을 .class 파일에 저장하지만, 런타임에는 사용할 수 없다. 하지만 대부분의 어노테이션은 런타임에도 사용할 수 있어야 하므로 코틀린에서는 기본적으로 어노테이션의 @Retention을 RUNTIME으로 지정한다.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy

@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}

자바와는 달리 코틀린에서는 애노테이션의 인자로 클래스나 함수선언, 타입 외에 임의의 식을 허용한다. 가령 @Suppress 는 컴파일러 경고를 무시하기 위한 애노테이션이다.

@Suppress(“UNCHECKED_CAST”)
val String = list as List<String>

Creating custom annotation

아래의 어노테이션 예제는 클래스 레벨 제약조건(Constraint)를 걸기 위해서 작성한 어노테이션이다.

@Target(
AnnotationTarget.ANNOTATION_CLASS,
AnnotationTarget.CLASS
)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [KarolValidator::class])
annotation class ValidKarol(
val message: String = “Your custom message”,
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)
  • 기존의 자바에서의 어노테이션과 다르게 코틀린의 어노테이션의 내부 변수 선언은 primary constructor를 이용한다.
  • 기존의 자바에서는 일반 메서드를 선언하듯 String message()와 같이 선언했었다. 다음과 같은 타입을 primary constructor에 선언할 수 있다. Java 기준 primitive type: Int, Long, Double, Char 등등, String, Enum, KClass 타입, 위의 언급한 선언 가능한 타입의 Array
  • 기본값 선언 방법이 다르다. 자바에서 기본값 선언은 default 키워드를 이용했다. 예를 들어 message 라는 변수에 기본값을 주는 방법은 String message () default “Your custom message” 정도였다.
  • 하지만 코틀린은 일반 변수에 기본값 선언하듯 val message: String = “Your custom message” 이렇게 선언해도 된다.
  • 클래스 타입을 사용하는 방법이 바뀌었다. 자바에서는 Class로 클래스 타입을 선언 후 이용하였다. 예를 들면 Class<?>[] groups () default {} 로 선언했었다.
  • 하지만 코틀린으로 넘어오면서 Class는 코틀린 클래스인 KClass와 자바 클래스인 Class를 모두 이용할 수 있게 되었다. 여기서 어노테이션에서는 자바 클래스인 Class는 사용하지 못한다. 대신, 코틀린에서는 코틀린 클래스인 KClass를 ```val groups: Array<KClass<*>> = []``` 와 같이 선언해주면 되겠다.
  • 자바라면 다음과 같이 선언했을 것이다.

@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = KarolValidator.class)
@interface ValidKarol {
String message () default “Your custom message”;
Class<?>[] groups () default {};
Class<? extends Payload>[] payload () default {};
}

Controlling Java API with annotations

* 코틀린은 코틀린으로 선언한 내용을 자바 바이트코드로 컴파일하는 방법과 코틀린 선언을 자바에 노출하는 방법을 제어하기 위한 애노테이션들을 제공한다.
* 애노테이션의 선언은 annotation class 로 부터 시작되는데, 애노테이션 클래스는 선언이나 식과 관련된 메타데이터의 구조를 정의하는 용도이기 때문에 클래스의 본문을 정의할 수 없다.
* 따라서 파라미터가 존재하는 애노테이션을 정의하려면 주 생성자에 파라메터를 다음과 같이 선언해야 한다.

/**
* 아래 선언된 애노테이션은 제이키드의 애노테이션중 하나이다.
* 아무런 파라미터도 없는 가장 단순한 애노테이션의 형태
* 애노테이션 선언 구문은 class 키워드앞에 annotation 변경자만 붙여주면 된다.
*/
annotation class JsonExclude

/**
* 애노테이션은 선언이나 식과 관련된 메타데이터 구조만을 정의한다.
* 따라서 본문을 정의할 수 없으며, 파라미터가 존재하는 애노테이션을 정의하려면
* 주 생성자에 파라미터를 정의해야 한다.
*/
annotation class JsonName(val name: String)

자바로 선언하면 다음과 같다. value는 이름을 생략할 수 있는 특별한 메소드이다. 자바와 달리, 코틀린에서 애노테이션을 적용할 때는 일반적인 생성자 호출과 동일하다.

public @interface JsonName {
String value();
}

kotlin.jvm 패키지에 있는 코틀린 코드를 자바에서 활용할 때 유용하고 자주 사용되는 어노테이션에 대해서 알아보자.

@JvmName
* @JvmName 은 Java의 클래스 또는 메소드의 이름을 지정한다.
* 보통 @JvmName 은 Kotlin 메서드 또는 클래스를 JVM 바이트코드로 변환 하고 Java에서 호출되는 Kotlin 함수나 클래스의 이름이 동일한 시그니처일 때 이를 처리하기 위해서 사용한다.
* 아래 코드는 같은 이름을 가진 함수의 다형성 예시이다.

fun foo(names: List<String>): List<String>
fun foo(numbers: List<Int): List<Int>

위 코드를 컴파일 하면 아래와 같은 에러를 발생하게 된다.

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

이유는 바이트코드 생성시 List의 Generic 값은 구분되지 않기 때문에 두개가 동일한 메서드로 판단되기 때문이다. 이럴 때 @JvmName 어노테이션을 사용하여 이름을 지정해주면 해결된다.

@JvmName(name = “names”)
fun foo(names: List<String>): List<String>
@JvmName(name = “numbers”)
fun foo(numbers: List<Int): List<Int>

@JvmField

Kotlin 컴파일러에게 @JvmField 어노테이션을 선언한 프로퍼티에 대해 getters/setters를 생성하지 않게 요청한다.

@JvmField 어노테이션 사용 제약은 다음과 같습니다.
1. backing field를 가지고 있어야함
2. private 는 불가
3. open override const 키워드가 아닌 경우
4. delegate 프로퍼티가 아닌 경우

class Store(val name: String, @JvmField val latitude: Long)
  • Java로 치면 다음과 같다.
public void main() {
Store store = new Store(“Cafe”, 128.1234567)
String name = store.getName();
long latitude = store.latitude;
}

@JvmStatic

* Kotlin에서 companion object를 사용하여 위와 같이 구성한 코드를 자바에서 사용하려면 속성 및 함수가 자바의 필드/메서드로 해석되도록 알려주어야 한다.
* const 선언이 되어 있는 속성은 별도 처리가 필요 없이 자바에서도 동일하게 사용 가능하며, 함수는 @JvmStatic 어노테이션을 사용하여 자바에서 정적 메서드로 사용할 수 있게 한다.

// https://www.androidhuman.com/2016-07-10-kotlin_companion_object
class Foo {
companion object {
// 자바에서도 동일하게 Foo.BAR 로 접근 가능
const val BAR = “bar
// 자바에서 정적 메서드(static method)처럼 사용할 수 있도록 함
@JvmStatic fun baz() {
// Do something
}
}
}
public void doSomething(Bundle args) {
// args 내에 “bar” 키로 정의되어 있는 데이터 추출
int bar = args.getInt(Foo.BAR);
// baz() 메서드 수행
Foo.bar();
}

@JvmOverloads

자바에서 생성자 오버로딩을 해 보자.

public class CustomView extends View {
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

* 코틀린으로 표현하면 다음과 같다.

class CustomView : View {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

// …
}
```

사용법: 클래스의 헤더 옆에 @JvmOverloads를 붙이면 된다.

class SubClassName @JvmOverloads constructor(매개변수 목록) : SuperClassName(인자 목록)

* 코드가 길기에, @JvmOverloads를 이용하여 간결하게 리팩토링하자.


class CustomView @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {

annotation class

* 클래스 참조를 파라미터로 하는 애노테이션을 사용하면, 클래스를 선언 메타데이터로 참조할 수 있다.
* 클래스를 참조타입으로 선언하려면 KClass 타입을 사용해야 하며, 이는 자바의 java.lang.Class 타입과 같은 역할을 한다.


```kotlin
// https://ncucu.me/186
/**
* 클래스 참조를 파라미터로 하는 애노테이션을 선언하면 클래스를 선언 메타 데이터로 참조할 수 있다.
* 클래스를 참조타입으로 선언하려면 KClass 라는 타입을 사용해야한다.
* 이는 자바의 java.lang.Class 와 같은 역할을 하는 코틀린 클래스이다.
*/
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
```

* 자바에서 ```@interface``` 라는 다소 모호한 이름으로 선언하던 것과 달리 확실히 annotation 클래스라는 것을 명시해주고 있다.
* 애노테이션 클래스는 선언이나 식과 관련 있는 메타데이터의 구조를 정의하기 때문에 내부에 어떤 코드도 들어갈 수 없다. 만약 파라미터가 있는 애노테이션을 적용하고자 한다면 애노테이션 클래스의 주 생성자에 파라미터를 선언해야 한다.
* 제네릭 클래스를 인자로 받아야 한다면 ```KClass<out 클래스명<*>>``` 을 사용해야 한다. 해당 제네릭에 어떤 타입이 올 지 모르기 때문이다.


```kotlin
interface Animal<T>
annotation class AnnotationGenericClassParameter(
val targetClass: KClass<out Animal<*>>
)
```

Meta-annotation

  • 애노테이션 클래스에 적용할 수 있는 애노테이션을 메타애노테이션이라고 부른다.
  • 표준 라이브러리에서 가장 일반적으로 쓰이는 메타애노테이션을 꼽으라면 당연 ```@Target``` 애노테이션일 것이다.
  • ```@Target``` 메타애노테이션은 애노테이션을 적용할 수 있는 요소의 유형을 지정한다. 가령 클래스인지, 메소드, 프로퍼티인지 같은 것들을 명시해줄 수 있다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // Spring will see this and treat @Service in the same way as @Component
public @interface Service {

@Target

  • @Target은 Java compiler 가 annotation 이 어디에 적용될지 결정하기 위해 사용합니다.
  • 예를 들어 위에서 사용한 ```@Service``` 의 ```ElementType.TYPE``` 은 해당 Annotation 은 타입 선언 시 사용한다는 의미입니다.
  • 종류는 다음과 같습니다.

    ElementType.PACKAGE : 패키지 선언
    ElementType.TYPE : 타입 선언
    ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
    ElementType.CONSTRUCTOR : 생성자 선언
    ElementType.FIELD : 멤버 변수 선언
    ElementType.LOCAL_VARIABLE : 지역 변수 선언
    ElementType.METHOD : 메서드 선언
    ElementType.PARAMETER : 전달인자 선언
    ElementType.TYPE_PARAMETER : 전달인자 타입 선언
    ElementType.TYPE_USE : 타입 선언

@Retention

  • @Retention``` 은 Annotation이 실제로 적용되고 유지되는 범위를 의미합니다.
  • Policy 에 관련된 Annotation 으로 컴파일 이후에도 JVM 에서 참조가 가능한 RUNTIME 으로 지정합니다.
  • 종류는 다음과 같습니다.

    RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM 에 의해서 계속 참조가 가능합니다. 주로 리플렉션이나 로깅에 많이 사용됩니다.
    RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유효합니다.
    RetentionPolicy.SOURCE : 컴파일 전까지만 유효합니다. 즉, 컴파일 이후에는 사라지게 됩니다.

결론

참고

  • 실제로 Built-in Annotaiton 을 이용해서 Spring 이나 Java 내에서도 해당 기능이나 상세 내용을 알지 못해도 쉽게 사용할 수 있도록 되어 있다.
  • 이처럼 해당 Interface 가 캡슐화와 ISP(Interface Segregation Principle) 이 잘되어 있기 때문에 필요한 기능만 손쉽게 사용할 수 있다고 생각한다.
  • 실무에서도 Annotation 이나 Interface 를 설계할 때 해당 meta-anntation 을 사용하면 원하는 기능을 Client 에게 제공할 수 있다.

리플렉션 : 실행 시점에 코틀린 객체 내부 관찰

* 리플렉션(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 로 변환할 수 있다.
*/
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
```

Kotlin Reflection API interfaces

정리

* 코틀린에서 애노테이션을 적용할때 문법은 자바와 거의 동일하다.
* 코틀린에서는 자바보다 더 넓은 대상에 애노테이션 적용이 가능하다.
* 애노테이션 인자로 원시 타입 값, 문자열, 이넘, 클래스 참조, 다른 애노테이션 클래스의 인스턴스, 배열 을 사용할 수 있다.
* ```@get:Rule``` 을 사용해 애노테이션의 사용 대상을 명시하면 코틀린 선언이 여러가지 바이트코드 요소를 만들어내는 경우 정확히 어떤 부분에 애노테이션을 적용할지 지정이 가능하다.
* 애노테이션 클래스 정의시 본문이 없어야하고, 주 생성자의 모든 파라미터는 val 이여야 한다.
* 메타애노테이션을 사용해 애노테이션 유지 방식등 특성 지정이 가능하다.
* 리플렉션 API는 자바의 리플렉션, 코틀린의 리플렉션 API 두가지를 모두 다루어야한다.
* 코틀린 리플렉션 API 의 경우 한계가 있기 때문에 자바의 리플렉션 API를 사용해야할 일이 생긴다.

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/
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) : Companydata 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)) // 프로퍼티 값을 얻는다.
}
}
```

Reference

--

--

Responses (3)