오늘도 더 나은 코드를 작성하였습니까?

애노테이션 만들기 본문

Kotlin in Action/코틀린 답게 사용하기

애노테이션 만들기

hik14 2021. 3. 5. 15:34

애노테이션 선언

제이키드 Json 직렬화 라이브러리 (코틀린 액션 제공 소스코드)

 

@JsonExclude 는 아무 파라미터도 없는 가장 단순한 애노테이션이다.

 

annotation class JsonExclude

annnotation 키워드만 제외하면 일반 클래스와 선언이 비슷하다.

애노테이션 클래스는 선언이나 식과 관련하여 메타데이터를의 구조를 정의 하기 때문에 내부에 아무코드도 없을때가 있다.

 

파라미터를 애노테이션이 가지려면,

annotationn class JsonName(val name: String)

annotationn class JsonName(val name: String)

일반 클래스 구문과 동일하게 작성하되 모든 파라미터에 val로 선언을 해야된다.

public @interface JsonName{
    String value();
}

val name:String 대신 자바 애노테이션 선언 value 메소드를 사용한점을 유의하자. 자바의 value()는 특별하다. 

애노테이션을 적용할때 value를 제외한 모든 애트리뷰트에는 이름을 명시해야된다.

 

반면 코틀린의 애노테이션 적용 문법은 일반적인 생성자 호출과 동일하다. 따라서 인자에 이름을 명하기 위해 이름붙인 인자를 사용할 수도 있고 이름을 생략할 수도 있다.

@Jsonname(name = "first_name")
@Jsonname("first_name")

자바에서 선언한 애노테이션을 코틀린에서 사용할 경우, value를 제외한 모든 인자에 이름을 붙혀서 사용해야된다.

 

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

애노테이션 클래스에 애노테이션을 붙힐수가 있는데, 이것을 메타 애노테이션이라 부른다.

메타 에노테이션은 컴파일러가 애노테이션을 처리를 제어한다. 일반 애노테이션은 선언 및 식을 제어한다.

 

표준라이브러리에는 몇 가지 메타애노테이션이 있으며,  프레임워크중에도 메타 애노테이션을 제공한다

*의존성 주입 라이브러리 -> 메타애노테이션을 이용해 주입 가능한 타입이 동일한 여러 객체를 식별한다.

 

예시)

@Target - 선언된 애노테이션이 적용될 수 있는 요소 유형을 정의한다.

- 아무것도 없으면 모든 요소에 적용가능하다.

- 제이키드 라이브러리는 프로퍼티에 애노테이션만을 사용하기 때문에 애노테이션클래스에 @Target을 꼭 지정해야 한다.

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

 

AnnotationTarget은 enum이고 여기에 애노테이션이 붙을수 있는 대상을 정의해 놓았다.

public enum class AnnotationTarget {
    /** Class, interface or object, annotation class is also included */
    CLASS,
    /** Annotation class only */
    ANNOTATION_CLASS,
    /** Generic type parameter (unsupported yet) */
    TYPE_PARAMETER,
    /** Property */
    PROPERTY,
    /** Field, including property's backing field */
    FIELD,
    /** Local variable */
    LOCAL_VARIABLE,
    /** Value parameter of a function or a constructor */
    VALUE_PARAMETER,
    /** Constructor only (primary or secondary) */
    CONSTRUCTOR,
    /** Function (constructors are not included) */
    FUNCTION,
    /** Property getter only */
    PROPERTY_GETTER,
    /** Property setter only */
    PROPERTY_SETTER,
    /** Type usage */
    TYPE,
    /** Any expression */
    EXPRESSION,
    /** File */
    FILE,
    /** Type alias */
    @SinceKotlin("1.1")
    TYPEALIAS
}

필요하다면 2개 이상의 대상을 지정할 수도 있다.

* 대상을 PROPERTY로 지정한 애노테이션을 자바에서 사용못하고,  AnnotationTarget.FIELD를 정의해 주어야 된다. 

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)

 

메타애노테이션을 직접 만들어야된다면, 다음과 같이 하면된다.

@Target(AnnotationTarget.ANNOTATION_CLASS) // 메타에노테이션을 통한 메타애노테이션 클래스 정의
annotation class BindingAnnotation

@BindingAnnotation // 위에서 정의한 메타에노테이션을 이용해 애노테이션 클래스 정의
annotation class MyBinding 

 

**@Retention 애노테이션,

-  정의 한 애노테이션 클래스를 소스 수준에 유지,  .class파일 저장, 실행시점에 리플렉션을 사용해 접근할 수 있게 할지, 지정한다.

-  자바 컴파일러는 기본적으로 애노테이션을 .class파일에 저장하고, 런타임(실행시점)에  사용할 수 없도록 한다. 

- 대부분의 애노테이션을 런타임에도 사용을 필요로 해서 디폴트로 코틀린에서는 @Retention을 RUNTIME으로 지정한다.

 

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

특정 클래스를 선언  메타데이터로 참조할 수 있는 기능이 필요할 때도 있다.

클래스 참조를 파라미터로 하는 애노테이션 클래스를 선언 하면 가능하다.

 

제이키드의 @DeserializeInterface는 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할때 사용한다.

인터페이스를 직접 만들수는 없고, 역직렬화 시 어떤 클래스를 사용해 인터페이스를 구현할지는 지정할 수 있어야 한다.

interface Company {
    val name: String
}

data class CompanyImpl(override val name: String): Company

data class Person(
    val name: String,
    @DeserializeInterface(CompanyImpl::class) company: Company // 일반적으로 클래스를 참조하기위해  ::class 를 뒷붙힌다.
)

직렬화된 person 인스턴스를 역직렬화 하는 과정에서 company 프로퍼티를 표현한 JSON을 읽으면 제이키드는 그 프로퍼티 값에 해당하는 JSON을  역직렬화 하면서 CompanyImpl의 인스턴스를 만들어서 Person 인스턴스의 company 프로퍼티에 설성한다

 

역직렬화시

Person{name, Company{...} } (Json) ---> Company{ ... } (Json)--->CompanyImpl(객체)----> Person(name, CompanyImpl) (객체)

annotation class DeserializeInterface(val targetClass: KClass<out Any>)

KClass는 자바 java.lang.Class 타입과 같은 역활을 한다. 코틀린 클래스에 대한 참조를 저장할때 KClass타입을 쓴다.

KClassd의 타입 파라미터는 이 KClass의 인스턴스가 가리키는 코틀린 타입을 지정한다. 

 CompanyImpl::class ---> KClass<CompanyImpl>

 

KClass의 타입파라미터를 쓸데 out변경자 없이 KClass<Any>라고 쓰면  CompanyImpl::class을 넘길수 없고 오로지 Any::Class만 가능하다. out을 변경자를 붙혀줌으로 코틀린 타입 T에 대해 KClass<T>가 KClass<out Any>하위 타입이기만 하면 된다. 

 

애노테이션 파라미터로 제네릭 클래스 사용하기

기본적으로 제이키드는 원시 타입이 아닌 프로퍼티를 중첩된 객체로 직렬화 한다.

@CumstomSerializer 애노테이션은 커스텀 직렬화 클래스에 대한 참조를 인자로 받는다.

인자로 쓰이는 커스텀 직렬화 클래스는 ValueSerializer<T>인터페이스를 구현해야된다.

interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any? // 직렬화 
    fun fromJsonValue(jsonValue:Any?):T // 역직렬화
}

 

날짜를 직렬화 해보자.

ValueSerializer<Date>를 구현하는 DateSerializer를 만들어서 인자로 애노테이션에 넘겨서 사용하자.

data class Person(
    val name: String,
    @CustomSerializer (DateSerializer::class) val birthDate: Date
)
annotation class CustomSerializer(
    val serializerClass: KClass<out ValueSerializer<*>>)

KClass<out ValueSerializer<*>>

1. ValueSerializer를 구현한 하위 타입의 참조를 저장한다.

2. ValueSerializer는 <*>을 통해 모든 타입을 직렬화 한다.

 

일반 클래스를 인자로 받는 패턴과 같다.

 

제네릭 클래스를 인자로 받아야한다면, KClass<out 허용할 클래스<*>>것처럼 허용할 클래스의 이름 뒤에 스타 프로젝션을  덧붙힌다