[Kotlin in Action] part10_애노테이션과 리플렉션
애노테이션과 리플렉션을 사용하면 소스코드에서 함수와 클래스 이름을 알지 못해도 임의의 클래스를 다룰 수 있다.
애노테이션 선언과 적용
애노테이션 적용
애노테이션을 적용할 때는 @과 애노테이션 이름을 입력하면 된다.
ex_제이유닛 프레임워크(http://junit.org/junit4/) -> @Test 애노테이션 붙이기
@Deprecated 애노테이션 : replaceWith 파라미터를 사용하는 패턴
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }
ㄴ 애노테이션에 인자를 넘길 때는 일반 함수처럼 괄호 안에 인자를 넣는다.
[애노테이션 인자를 지정하는 문법]
- 애노테이션 인자가 클래스일 때: @MyAnnotation(MyClass::class) 처럼 ::class를 클래스 이름 뒤에 넣는다.
- 애노테이션 인자가 애노테이션일 때: 들어가는 애노테이션 앞에는 @를 넣지 않는다.
- 애노테이션 인자가 배열일 때: @Request-Mapping(path = arrayOf("/foo", "/bar")) 처럼 arrayOf 함수를 사용함.
애노테이션 대상
사용 지점 대상 : 애노테이션을 붙일 요소를 정할 수 있다.
사용 지점 대상은 @ 기호와 애노테이션 이름 사이에 붙고, 애노테이션 이름과는 콜론(:)으로 분리된다.
@get:Rule을 사용해야 @Rule 애노테이션을 예외없이 정확한 대상에 적용할 수 있다.
자바에 선언된 애노테이션을 사용해서 프로퍼티에 애노테이션을 붙일 때와 달리
코틀린으로 애노테이션을 선언하면 프로퍼티에 직접 적용할 수 있는 애노테이션을 만들 수 있다.
- property - 프로퍼티 전체. 자바에서 선언된 애노테이션에는 이 사용 지점 대상을 사용할 수 없음.
- field - 프로퍼티에 의해 생성되는 필드
- get - 프로퍼티 게터
- set - 프로퍼티 세터
- receiver - 확장 함수나 프로퍼티의 수신 객체 파라미터
- param - 생성자 파라미터
- setparam - 세터 파라미터
- delegate - 위임 프로퍼티의 위임 인스턴스를 담아둔 필드
- file - 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스
애노테이션을 활용한 JSON 직렬화 제어
직렬화 - 객체를 저장장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것
역질렬화 - 텍스트나 이진 형식으로 저장된 데이터로부터 원래의 객체를 만들어낸다.
JSON - 직렬화에 자주 쓰이는 형식
ㄴ 자주 쓰이는 : 잭슨(https://github.com/FasterXML/jackson), 지슨(https://github.com/google/gson)
애노테이션을 활용하면 객체를 직렬화하거나 역직렬화하는 것을 제어할 수 있다.
- @JsonExclude 애노테이션을 사용하면 직렬화나 역직렬화할 때 프로퍼티 무시 가능
- @JsonName애노테이션을 사용하면 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 애노테이션 이름 씀
애노테이션 선언
@JsonExclude를 사용하여 애노테이션 선언
아무 파라미터도 없음
annotation class JsonExclude
ㄴ 애노테이션 코드는 내부에 어떤 코드도 들어올 수 없다.
-> 파라미터가 있는 애노테이션을 정의하려면 애노테이션 클래스의 주 생성자에 파라미터 선언
annotation class JsonName(val name: String)
메타애노테이션: 애노테이션을 처리하는 방법 제어
메타애노테이션 - 애노테이션 클래스에 적용할 수 있는 애노테이션
-> 표준 라이브러리의 메타애노테이션 - 컴파일러가 애노테이션을 처리하는 방법을 제어
=> @Target - 애노테이션 적용 가능 대상을 지정
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
-> 애노테이션이 붙을 수 있는 대상이 정의된 이넘 AnnotationTarget
=> 둘 이상의 대상을 한 번에 선언 가능
@Target(AnnotationTarget.CLASS, AnnotationTarget.METHOD)
애노테이션 파라미터로 클래스 사용
-> 어떤 클래스를 선언 메타 데이터로 참조할 수 있는 기능이 필요할 때
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 - 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 사용
=> 역직렬화 시 어떤 클래스를 사용해 인터페이스를 구현할지를 지정할 수 있어야 함.
애노테이션 파라미터로 제네릭 클래스 받기
제이키드는 기본적으로 원시 타입이 아닌 프로퍼티를 중첩된 객체로 직렬화한다.
-> 이를 변경하고 싶으면 값을 직렬화하는 로직을 직접 제공
=> @CustomSerializer 애노테이션
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
리플렉션: 실행 시점에 코틀린 객체 내부 관찰
리플렉션 - 실행 시점에 동적으로 객체의 프로퍼티와 메서드에 접근할 수 있게 해주는 방법
-> JSON 직렬화 라이브러리 - 어떤 객체든 JSON으로 변환할 수 있어야. 실행 시점 전까지 라이브러리가 직렬화할 프로퍼티나 클래스의 정보를 알 수 없음.
ㄴ 이 경우 리플렉션 사용.
-> 두 가지 서로 다른 리플렉션 API 다뤄야 함.
- 자바가 java.lang.reflect 패키지를 통해 제공하는 표준 리플렉션
- 코틀린이 kotlin.reflect 패키지를 통해 제공하는 코틀린 리플렉션 API
코틀린 리플렉션 API: KClass, KCallable, KFunction, KProperty
KClass - java.lang.Class에 해당. 클래스 안의 모든 선언을 열거하고 각 선언에 접근하거나 클래스의 상위 클래스를 얻는 등의 작업이 가능하다.
-> KClass의 인스턴스 얻기: MyClass::class 식으로,
-> 자바 클래스를 얻으면 -> .kotlin확장 프로그램을 통해 자바에서 코틀린 리플렉션 API로 옮겨올 수 있다.
class Person(val name: String, val age: Int)
>>> val person = Person("Alice", 29)
>>> val kClass = person.javaClass.kotlin
>>> println(kClass.simpleName)
Person
>>> kClass.memberProperties.forEach { println(it.name) }
age
name
KCallabe - 함수와 프로퍼티를 아우르는 공통 상위 인터페이스
ㄴ의 call 메서드 : 함수나 프로퍼티의 게터를 호출할 수 있다.
interface KCallable<out R> {
fun call(vararg args: Any?): R
...
}
fun foo(x: Int) = println(x)
>>> val kFunction = ::foo
>>> kFunction.call(42)
42
[함수를 호출하기 위한 구체적인 메서드]
-> ::foo 식의 값 타입이 리플렉션 API에 있는 KFunction 클래스의 인스턴스
::foo의 타입 - KFunction<Int, Unit>에는 파라미터와 반환 값 타입 정보가 들어있음
->KProperty의 call 메서드 호출
call은 프로퍼티의 게터를 호출
var counter = 0
>>> val kProperty = ::counter
>>> kProperty.setter.call(21)
>>> println(kProperty.get())
21
리플렉션을 사용한 객체 직렬화 구현
제이키드의 직렬화 함수 선언
fun serialize(obj: Any): String
-> 직렬화 함수의 기능
=> 기본적으로 직렬화 함수는 객체의 모든 프로퍼티를 직렬화한다.
private fun StringBuilder.serializeObject(obj: Any) {
val kClass = obj.javaClass.kotlin
val properties = kClass.memberProperties
properties.joinToStringBuilder(
this, prefix = "{", postfix = "}") { prop ->
serializeString(prop.name)
append(": ")
serializePropertyValue(prop.get(obj))
}
}
애노테이션을 활용한 직렬화 제어
JSON 직렬화 과정을 제어하는 과정 중에서 @JsonExclude를 사용하여 특정 필드들을 제외하고 싶을 경우
private fun StringBuilder.serializeObject(obj: Any) {
obj.javaClass.kotlin.memberProperties
.filter { it.findAnnotation<JsonExclude>() == null }
.joinToStringBuilder(this, prefix = "{", postfix = "}") {
serializeProperty(it, obj)
}
}
이 코드는 @JsonExclude로 애노테이션한 프로퍼티를 제외시킨다.
JSON 파싱과 객체 역직렬화
JSON 문자열 입력을 파싱하고, 리플렉션을 사용해서 객체의 내부에 접근해 새로운 객체와 프로퍼티를 생성
ㄴ JSON 역직렬화는 직렬화보다 어렵다.
1. 어휘 분석기(렉서lexer) - 입력 문자열을 토큰(문자 토큰, 값 토큰)의 리스트로 변환
2. 문법 분석기(파서parser) - 토큰의 리스트를 구조화된 표현으로 변환
3. 역직렬화 컴포넌트 - 파싱한 결과로 객체를 생성
최종 역직렬화 단계 : callBy(), 리플렉션을 사용해 객체 만들기