책정리/Kotlin in Action

[Kotlin in Action] 애노테이션과 리플렉션

뽀글보리 2021. 11. 23. 22:14
반응형

이 포스팅은 Kotlin in Action 책의 10장을 정리한 글 입니다.

 

  • Annotation과 Reflection을 사용하면 실행 시점에 컴파일러 내부 구조를 분석할 수 있다.

10.1 애노테이션 선언과 적용

10.1.1 애노테이션 적용

  • 자바와 똑같은 방식으로 @를 붙여서 사용
@Deprecated("Use removeAt(index) instead", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }
  • 자바와의 차이
    • 클래스를 애노테이션 인자로 지정할 때 @MyAnnotaion(MyClass::class)
    • 인자로 들어가는 애노테이션의 이름 앞에는 @를 넣지 않는다.
    • 배열을 인자로 지정하려면 arrayOf 함수를 사용한다.
  • 어노테이션 인자는 컴파일 시점에 알 수 있어야 한다.
    • 따라서 프로퍼티는 안되고 const 프로퍼티는 된다.

10.1.2 애노테이션 대상

  • 애노테이션 선언 하나가 여러 개의 자바 선언과 대응할 수 있다.
    • 예를 들어 프로퍼티에 붙이면 getter, setter에도 ...
  • 사용 지점 대상 선언을 사용할 수 있다.
    • @get:MyAnnotation getter에만 어노테이션이 붙는다.
    • property, field, get, set, receiver, param, setparam, delegate(위임), file
    • 파일에 있는 최상위 선언을 담는 클래스의 이름을 바꿔주는 JvmName - 3.2.3 절 참고
      import strings.StringFunctions;
      StringFunctions.joinToString(list, "")
    • @file:JvmName("StringFunctions") package strings fun joinToString(...): String {...}

10.1.3 애노테이션을 활용한 JSON 직렬화 제어

  • 직렬화 Person("Alice", 29) → {"age": 29, "name": "Alice"}
  • 역직렬화 {"age": 29, "name": "Alice"} → Person("Alice", 29)
  • @JsonExclude 직렬화/역직렬화 시 프로퍼티 무시
  • @JsonName 다른 지정 이름을 쓸 수 있음
  • 제이키드 라이브러리에서 제공

10.1.4 애노테이션 선언

  • 애노테이션 클래스는 메타데이터의 구조를 정의하므로 내부에 아무 코드도 들어있을 수 없다
pubic @interface JsonName {
  String value();
}
annotation class JsonName(val name: String)
@JsonName(name= "first_name")
@JsonName("first_name")

10.1.5 메타애노테이션: 애노테이션을 처리하는 방법 제어

  • 애노테이션 클래스에도 애노테이션을 붙일 수 있다 → Meta-Annotation
  • @Target 애노테이션을 적용할 수 있는 요소의 유형을 지정한다.
  • 주의할 점 ? AnnotationTarget.PROPERTY와 AnnotationTarget.FIELD는 다르다.
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
  • @Retention 코틀린에서는 기본적으로 RUNTIME으로 지정한다.

10.1.6 애노테이션 파라미터로 클래스 사용

  • 정적인 데이터 외에도 클래스를 참조할 필요가 있을 수 있다.
  • 클래스 참조를 파라미터로 하는 애노테이션 클래스 선언
  • @DeserializeInterface(CompanyImpl::class)
  • annotation class DeserializeInterface(val targetClass: KClass<out Any>)
  • out을 쓰지 않으면 Any::class만 넘길 수 있다.
  • KClass는? 10.2 참조

10.1.7 애노테이션 파라미터로 제네릭 클래스 받기

  • KClass<out 클래스이름<*>>

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

  • 리플렉션 : 실행 시점에 동적으로 객체의 프로퍼티와 메소드에 접근할 수 있게 해주는 방법
  • 보통 메서드/프로퍼티 접근시에 컴파일 시점에 이름을 찾아내서 존재를 보장함
  • 그러나 JSON 직렬화 경우 프로퍼티 이름을 오직 실행 시점에만 알 수 있다.
  • 코틀린에서 리플렉션 사용하려면..
    • java.lang.reflect 패캐지를 통해 제공하는 표준 리플렉션 API
      • 자바/코틀린 코드 완전히 호환
    • kotlin.reflect 패캐지를 통해 제공하는 코틀린 리플렉션 API
      • 널이 될 수 있는 타입 등 지원
      • 그러나 복잡한 기능은 제공하지 않는다.. → 이건 자바 API로 해야됨

10.2.1 코틀린 리플렉션 API: KClass, KCallable, KFunction, KProperty

  • KClass
    • 클래스 안에 있는 모든 선언 접근/상위 클래스 얻는 작업
    • MyClass::class → KClass의 인스턴스 얻을 수 있다
    • 또는
    • import kotlin.reflect.full.* val person = Person("Alice", 29) val kClass = person.javaClass.kotlin // KClass<Person> 인스턴스 반환 kClass.memberProperties.forEach{ ... }
  • KCallable
    • 함수와 프로퍼티를 아우르는 공통 상위 인터페이스
    • call 메소드 → 함수나 프로퍼티의 게터 호출
  • KFunction
    • ::foo는 KFunction 클래스의 인스턴스
    • 만약 인자가 맞지 않는다면 IllegArgumentException이 날 것이다.
    • 더 구체적인 메소드를 사용하면 된다.
    • KFunction1<Int, Unit> → 틀리면 컴파일 못하게 → 차라리 invoke를 하면 됨 (타입 안맞으면 컴파일 안되게 한다.)
    • 인자 타입과 반환 타입을 모두 다 안다면 Invoke 메소드를 호출하는 게 낫다.
    • call 메소드는 모든 타입의 함수에 적용할수 있는 일반적인 메소드지만 타입 안전성을 보장해주지 않지만 , invoke는 타입 체크를 해준다.
    • kFunction.invoke(42)
  • fun foo(x: Int) = println(x) val kFunction = ::foo kFunction.call(42)
  • KProperty
    • KProperty에 call 메소드가 존재
    • var counter = 0 val kProperty = ::counter kProperty.setter.call(21) println(kProperty.get()) val person = Person("Alice", 29) val memberProperty = Person::age println(memberProperty.get(person))

10.2.2 리플렉션을 사용한 객체 직렬화 구현

  • serialize 함수는 작업을 serializeObject에 위임하고 StringBuilder 인스턴스.. → 람다 본문에서 인스턴스를 this로 사용할 수 있다는 장점
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
private fun StringBuilder.serializeObject(obj: Any) {
    val KClass = obj.javaClass.kotlin // get a kClass instance
    val properties = kClass.memberProperties // get all properties
    properties.joinToStringBuilder(
        this, prefix = "{", postfix = "}") { prop ->
            serializeString(prop.name) // get a property name
            append(": ")
            serializePropertyValue(prop.get(obj)) // get a property value
        }
}
  • 프로퍼티가 어떤 타입인지는 알 수 없기 때문에 각 prop의 타입은 KProperty1<Any, *>

10.2.3 애노테이션을 활용한 직렬화 제어

  • @JsonExclude, @JsonName 이런 애노테이션을 serializeObject 함수가 어떻게 처리할까?
  • KProperty에 findAnnotation 함수가 있다
  • @JsonExclude
    • val properties = kClass.memberProperties.filter { it.findannotation<JsonExclude> () == null }
  • @JsonName
    • val jsonNameAnn = prop.findAnnotation<JsonName>()
    • val propName = jsonNameAnn?.name ?: prop.name
  • 459쪽 CustomSerializer 만들기 → 몰라도 될 것 같음 PASS

10.2.4 JSON 파싱과 객체 역직렬화

  1. 어휘분석기 렉서
    1. 문자열을 토큰의 리스트로 변환한다. (보통 콤마)
  2. 문법 분석기 파서
    1. 토큰의 리스트를 구조화된 표현으로 변환한다.
    2. 의미 단위를 만날 때마다 JsonObject의 메소드를 적절히 호출한다.
  3. 파싱한 결과로 객체를 생성하는 역직렬화 컴포넌트 - 역직렬화기
    1. JsonObject 구현 제공
    2. 중첩된 객체 값을 만들어낸다.
    3. { person: {name: '1', age: 23}}
  • 구현해보기
fun <T: Any> deserialize(json: Reader, targetClass: KClass<T>): T {
    val seed = ObjectSeed(targetClass, ClassInfoCache())
  Parser(json, seed).parse()
  return seed.spawn()
}
  • ObjectSeed : 객체의 프로퍼티가 담겨져 있다.
  • 파서를 호출하면서 프로퍼티를 인자로 전달하고,
  • spawn 함수를 통해 결과 객체를 생성한다.
  • 세부적인 구현 → PASS

10.2.5 최종 역직렬화 단계: callBy(), 리플렉션을 사용해 객체 만들기

  • ClassInfo 클래스 : 최종 결과 객체 인스턴스를 생성하고 생성자 파라미터 정보를 캐시한다. ObjectSeed 클래스 안에서 사용된다.
  • callBy를 사용해서 프로퍼티 맵을 받으면 1) 파라미터 순서 신경쓰지 않아도 되고 2) 값이 없으면 디폴트 값을 넣어줄 수도 있다.
interface KCallable<out R>{
    fun callBy(args: Map<KParameter, Any?>): R
  • 그대신 type을 체크해 주어야 함 → KParameter.type 프로퍼티 활용
  • 값 타입에 따라 직렬화기 가져오기
fun serializerForType(type: Type): ValueSerializer<out Any?>? =
    when(type) {
        Byte::class.java -> ByteSerializer
        Int::class.java -> IntSerialzier
  • ClassInfoCache : 리플렉션 연산 비용을 줄이기 위한 클래스
class ClassInfoCache {
    private val cacheData = mutableMapOf<KClass<*>, ClassInfo<*>>
    operator fun <T:Any> get(cls: KClass<T>): ClassInfo<T> = 
        cacheData.getOrPut(cls) { ClassInfo(cls) } as ClassInfo<T>
  • cls에 대한 항목이 있다면 반환 → 그렇지 않으면 계산&저장 후 반환
  • 마찬가지로 생성자 파라미터나 애노테이션 인자로 지정한 클래스 등을 캐시해둘 수 있다.
  • 필수 파라미터가 모두 있는 검증하는 코드
private fun ensureAllParameter(arguments: Map<KParameter, Any?> {
    for (param in constructor.parameters) {
        if (arguments[param] == null &&
            !param.isOptional && !param.type.isMarkedNullable) {
            throw JKidException("Missing vlue for parameter ${params.name}")
  • 캐시를 사용하면? 모든 프로퍼티에 대해서 애노테이션을 찾는 과정을 반복할 필요가 없다!
반응형