📖 12.1 어노테이션 선언과 적용
🔖 12.1.1 어노테이션을 적용해 선언해 표지 남기기
- 코틀린에서 어노테이션을 적용하려면 @와 어노테이션 이름을 선언 앞에 넣으면 된다.
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) {
}
@Deprecated어노테이션은 최대 3가지 파라미터를 받는다.message: 사용중단 예고 이유replaceWith: 옛 버전을 대신할 수 있는 패턴 제시level: 점진적인 사용 중단을 지원 제공WARNING: 경고만 함ERROR: 컴파일되지 못하게 막음.HIDDEN: 컴파일되지 못하게 막으며, 예전에 컴파일된 코드와의 이진호환성만 유지
- 코틀린에서 어노테이션 인자를 지정하는 문법은 자바와 약간 다르다.
- 클래스를 어노테이션 인자로 지정:
::class를 클래스 이름 뒤에 넣어야 한다. - 다른 어노테이션을 인자로 지정: 인자로 들어가는 어노테이션의 이름 앞에
@를 넣지 말라. - 배열을 인자로 지정: 각괄호(
[])를 사용,arrayOf함수 사용
- 클래스를 어노테이션 인자로 지정:
- 어노테이션 인자를 컴파일 시점에 알 수 있어야 한다.
- 즉, 임의의 프로퍼티를 인자로 지정할 수는 없다.
- 프로퍼티를 인자로 사용하려면 const 변경자를 붙여야 한다.
🔖 12.1.2 어노테이션이 참조할 수 있는 정확한 선언 지정: 어노테이션 타깃
- 사용 지점 타깃 선언은 통해 어노테이션을 붙일 요소를 정할 수 있다.
- @ 기호와 어노테이션 이름 사이에 붙으며 어노테이션 이름과는 콜론으로 분리된다.
@get:JvmName("obtainCertificate")
@JvmName("performCalculation")
fun calculate(): Int {
return (2 + 2) - 1
}
calculate함수를 자바 쪽에서performCalculation()로 호출하게 한다.
class CertificateManager {
@get:JvmName("obtainCertificate")
@set:JvmName("putCertificate")
var certificate: String = "-----BEGIN CERTIFICATE-----"
}
certificate프로퍼티를obtainCertificate,putCertificate라는 이름으로 사용할 수 있다.
사용 지점 타깃을 지정할 때 지원하는 타깃 목록
- property: 프로퍼티 전체
- field: 프로퍼티에 의해 생성되는 필드
- get: 프로퍼티 게터
- set: 프로퍼티 세터
- receiver: 확장 함수나 프로퍼티의 수신 객체 파라미터
- param: 생성자 파라미터
- setparam: 세터 파라미터
- delegate: 위임 프로퍼티의 위임 인스턴스를 담아둔 필드
- file: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스
코틀린에는 컴파일러 경고를 무시하기 위한 @Suppress 어노테이션이 있다.
🔖 12.1.3 어노테이션을 활용해 JSON 직렬화 제어
- 직렬화는 객체를 저장 장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것
- 역직렬화는 텍스트나 이진 형식으로 저장된 데이터에서 원래의 객체를 만들어낸다.
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("Alice", 29)
println(serialize(person))
}
- Person 인스턴스를 serialize 함수에 전달하면 JSON 표현이 담긴 문자열을 돌려받는다.
fun main() {
val json = """{"name": "Alice", "age": 29"}"""
println(deserialize<Person>(json))
}
- JSON 표현을 다시 코틀린 객체로 만들려면 deserialize 함수를 호출한다.
data class Person(
@JsonName("alias") val firstName: String,
@JsonExclude val age: Int? = null
)
@JsonExclude어노테이션을 사용하면 직렬화나 역직렬화할 때 무시해야 하는 프로퍼티를 표시할 수 있다.- 기본값 지정 필요
@JsonName어노테이션을 사용하면 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 어노테이션이 지정한 문자열을 쓰게 할 수 있다.
🔖 12.1.4 어노테이션 선언
annotation class JsonExclude
- 어노테이션 클래스는 선언이나 식과 관련 있는 메타데이터의 구조만 정의하기 때문에 내부에 아무 코드도 들어있을 수 없다.
- 컴파일러는 어노테이션 클래스에서본문을 정의하지 못하게 막는다.
annotation class JsonName(val name: String)
- 파라미터가 있는 어노테이션을 정의하려면 어노테이션 클래스의 주 생성자에 파라미터를 선언해야 한다.
- 주 생성자 구문을 사용하며,
val로 선언
🔖 12.1.5 메타어노테이션: 어노테이션을 처리하는 방법 제어
- 메타어노테이션: 어노테이션 클래스에 적용할 수 있는 어노테이션
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
@Target메타어노테이션은 어노테이션을 적용할 수 있는 요소의 유형을 지정- 어노테이션 클래스에 대해 구체적인
@Target을 지정하지 않으면 모든 선언에 적용할 수 있는 어노테이션이 된다.
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation
@BindingAnnotation
annotation class MyBinding
- 메타어노테이션을 직접 만들어야 한다면
ANNOTATION_CLASS를 타깃으로 지정하자.
🔖 12.1.6 어노테이션 파라미터로 클래스 사용
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는 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 쓰는 어노테이션이다.
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
- 클래스 참조를 인자로 받는 어노테이션은 위와 같이 정의한다.
🔖 12.1.7 어노테이션 파라미터로 제네릭 클래스 받기
- 기본적으로 제이키드는 기본 타입이 아닌 프로퍼티를 내포된 객체로 직렬화한다.
- 이런 기본 동작을 변경하고 싶으면 값을 직렬화하는 로직을 직접 제공하면 된다.
interface ValueSerializer<T> {
fun toJsonValue(value: T): Any?
fun fromJsonValue(jsonValue: Any?): T
}
- 이 인터페이스는 코틀린 객체에서 JSON 표현으로의 변환을 제공하며, 반대의 경우도 제공한다.
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)
- 클래스를 어노테이션 인자로 받아야 할 때마다 같은 패턴 사용이 가능하다.
KClass<out 자신의 클래스 이름>
- 자신의 클래스 이름 자체가 타입 인자를 받아야 한다면
KClass<out 자신의 클래스 이름<*>>처럼 타입인자를 *로 바꾼다.
📖 12.2 리플렉션: 실행 시점에 코틀린 객체 내부 관찰
- 리플렉션은 실행 시점에 객체의 프로퍼티와 메서드에 접근할 수 있게 해주는 방법
🔖 12.2.1 코틀린 리플렉션 API: KClass, KCallable, KFunction, KProperty
interface KClass<T : Any> {
val simpleName: String?
val qualifiedName: String?
val members: Collection<KCallable<*>>
val constructors: Collection<KFunction<T>>
val nestedClasses: Collection<KClass<*>>
// ...
}
KClass선언을 찾아보면 클래스 내부를 살펴볼 때 사용할 수 있는 다양하고 유용한 메서드를 볼 수 있다.
interface KCallable<out R> {
fun call(vararg args: Any?): R
// ...
}
KCallable은함수와 프로퍼티를 아우르는 공통 상위 인터페이스다.call을 사용할 때는 함수 인자를vararg리스트로 전달
fun foo(x: Int) = println(x)
fun main() {
val kFunction = ::foo
kFunction.call(42)
}
::foo식의 값 타입이KCallable클래스의 인스턴스이다.
fun sum(x: Int, y: Int) = x + y
fun main() {
val kFunction = KFunction2<Int, Int, Int> = ::sum
println(kFunction.invoke(1, 2) + kFunction.invoke(3, 4))
kFunction(1)
// error
}
- invoke 메서드를 호출할 때는 인자 개수나 타입을 실수로 틀릴 수 없다. 컴파일이 안되기 때문
var counter = 0
fun main() {
val kProperty = ::counter
kProperty.setter.call(21)
println(kProperty.get())
}
- 최상위 읽기 전용과 가변 프로퍼티는 각각
KProperty0이나,KMutableProperty0인터페이스의 인스턴스로 표현- 인자 없는 get method 제공
KProperty1은 제네릭 클래스이다.
🔖 12.2.2 리플렉션을 사용해 객체 직렬화 구현
fun serialize(obj: Any): String
- 제이키드의 직렬화 함수 선언이다.
- 이 함수는 결과 JSON을
StringBuilder인스턴스 안에 구축한다.
private fun StringBuilder.serializeObject(x: Any) {
append(/*...*/)
}
- 별도로 수신 객체를 지정하지 않고 append 메서드를 편하게 사용할 수 있다.
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
- 대부분의 작업을
serializeObject에 위임한다.
private fun StringBuilder.serializeObject(obj: Any) {
val kClass = obj::class as KClass<Any>
val properties = kClass.memberProperties
properties.joinToStringBuilder(
this, prefix = "{", postfix = "}"
) { prop ->
serializeString(prop.name)
append(": ")
serializePropertyValue(prop.get(obj))
}
}
- 클래스의 각 프로퍼티를 차례로 직렬화 한다.
🔖 12.2.3 어노테이션을 활용해 직렬화 제어
어노테이션을 지원하기 위해 serializeObject를 어떻게 수정할지 알아본다.
val properties = kClass.memberProperties.filter { it.findAnnotation<JsonExclude>() == null }
- 위와 같이
@JsonExclude어노테이션이 붙지 않은 프로퍼티만 남길 수 있다.
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
- 어노테이션에 전달한 인자도 알아야 한다.
private fun StringBuilder.serializeObject(obj: Any) {
(obj::class as KClass<Any>)
.memberProperties
.filter { it.findAnnotation<JsonExclude>() == null }
.joinToStringBuilder(this, prefix = "{", postfix = "}") {
serializeProperty(it, obj)
}
}
@JsonExclude어노테이션한 프로퍼티를 제외시킨다.
private fun StringBuilder.serializeObject(prop: KProperty1<Any, *>, obj: Any) {
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
serializeString(propName)
append(": ")
serializePropertyValue(prop.get(obj))
}
@JsonName에 따라 프로퍼티 이름을 처리한다.
fun KProperty<*>.getSerializer(): ValueSerializer<Any?>? {
val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null
val serializerClass = customSerializerAnn.serializerClass
val valueSerializer = serializerClass.objectInstance
?: serializerClass.createInstance()
@Suppress("UNCHECKED_CAST")
return valueSerializer as ValueSerializer<Any?>
}
- 프로퍼티의 값을 직렬화하는 직렬화기 가져오기
private fun StringBuilder.serializeProperty(
prop: KProperty1<Any, *>, obj: Any
) {
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
serializeString(propName)
append(": ")
val value = prop.get(obj)
val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
serializePropertyValue(jsonValue)
}
serializeProperty구현 안에서getSerializer를 사용할 수 있다.
🔖 12.2.4 JSON 파싱과 객체 역직렬화
- 제이키드의 JSON 역직렬화기는 3단계로 구현돼 있다.
- 어휘 분석기(렉서)
- 토큰의 리스트로 변환(문자토큰, 값 토큰)
- 문법 분석기(파서)
- 토큰의 리스트를 구조화된 표현으로 변환
- 역직렬화 컴포넌트
- 필요한 클래스의 인스턴스를 생성해 반환
- 어휘 분석기(렉서)
🔖 12.2.5 최종 역직렬화 단계: callBy()와 리플렉션을 사용해 객체 만들기
interface KCallable<out R> {
fun callBy(args: Map<KParameter, Any?>): R
// ...
}
KCallable.call은 디폴트 파라미터 값을 지원하지 않는다.KCallable.callBy는 디폴트 파라미터 값을 지원한다.
object BooleanSerializer : ValueSerializer<Boolean> {
override fun fromJsonValue(jsonValue: Any?): Boolean {
if (jsonValue !is Boolean) throw JkidException("Boolean Expected")
return jsonValue
}
override fun toJsonValue(value: Boolean) = value
}
- Boolean 값을 위한 직렬화기
class ClassInfoCache {
private val cacheData = mutableMapOf<KClass<*>, ClassInfo<*>>()
@Suppress("UNCHECKED_CAST")
operator fun <T : Any> get(cls: KClass<T>): ClassInfo<T> =
cacheData.getOrPut(cls) { ClassInfo(cls) } as ClassInfo<T>
}
- 리플렉션 데이터 캐시 저장소
- 성능을 위해 검색 결과를 캐시에 넣어둔다.
