Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Jisoo’s devlog

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

Kotlin in Action

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

지 슈 2023. 1. 20. 15:54

애노테이션리플렉션을 사용하면 소스코드에서 함수와 클래스 이름을 알지 못해도 임의의 클래스를 다룰 수 있다.

 

애노테이션 선언과 적용

애노테이션 적용

애노테이션을 적용할 때는 @과 애노테이션 이름을 입력하면 된다.

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(), 리플렉션을 사용해 객체 만들기