Programming/Kotlin

코틀린(kotlin) - 애노테이션 선언과 적용

JunsuKim 2021. 12. 19.
728x90

에노테이션 적용

코틀린에선 자바와 같은 방법으로 애노테이션을 사용할 수 있다.

적용하려는 대상 앞에 애노테이션을 붙이는 것이다.

애노테이션은 @와 애노테이션 이름으로 이뤄지며 함수나 클래스 등 여러 다른 코드 구성 요소에 붙일 수 있다.

import org.junit.*
class MyTest {
    @Test fun testTrue() {
        Assert.assertTrue(true)
    }
}

애노테이션에 인자를 넘길 때는 일반 함수와 마찬가지로 괄호 안에 인자를 넣는다.

@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }

이런 remove 함수 선언이 있다면 remove를 호출하는 코드에 대해 경고 메세지를 출력할 뿐 아니라 자동으로 그 코드를 새로운 API 버전에 맞는 코드로 바꿔주는 퀵 픽스도 제시한다.

 

애노테이션의 인자로는 원시 타입의 값, 문자열, enum, 클래스 참조, 다른 애노테이션 클래스, 배열을 사용할 수 있다.

애노테이션 인자를 지정하는 문법을 봐보자.

  • 클래스를 애노테이션 인자로 지정할 때는 @MyAnnotation(MyClass::Class)처럼 ::class를 클래스 이름 뒤에 넣어야 한다.
  • 다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 @를 넣지 않아야 한다.
  • 배열을 인자로 지정하려면 @RequestMapping(path = arrayOf("/foo", "/bar"))처럼 arrayOf 함수를 사용한다.
  • 자바에서 선언한 애노테이션 클래스를 사용한다면 value라는 이름의 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환된다. 따라서 arrayOf 함수를 쓰지 않아도 된다.

 

애노테이션 인자를 컴파일 시점에 알 수 있어야 한다. (임의의 프로퍼티를 인자로 지정할 수는 없다.)

프로퍼티를 애노테이션 인자로 사용하려면 그 앞에 const 변경자를 붙여 상수로 취급해야 한다.

애노테이션 대상

사용 지점 대상(use-site target) 선언으로 애노테이션을 붙일 요소를 정할 수 있다.

사용 지점 대상은 @ 기호와 애노테이션 이름 사이에 붙으며, 애노테이션 이름과는 콜론(:)으로 분리된다.

 

규칙을 지정하려면 공개(public) 필드나 메소드 앞에 @Rule을 붙여야 한다.

하지만 코틀린 테스트 클래스의 folder라는 폴더 안에 @Rule을 붙이면 "The @Rule 'folder' must be public"이라는

제이유닛 예외가 발생한다.

@Rule 애노테이션을 정확한 대상에 적용하려면 @get : Rule을 사용해야 한다.

class HasTempFolder {
    @get : Rule
    val folder = TemporaryFolder()
    @Test
    fun testUsingTempFolder() {
        val createFile = folder.newFile("myfile.txt")
        val createdFolder = folder.newFolder("subfolder")
        // ...
    }
}

자바에 선언된 애노테이션을 사용해 프로퍼티에 애노테이션을 붙이는 경우 기본적으로 프로퍼티의 필드에 그 애노테이션이 붙지만, 코틀린으로 애노테이션을 선언하면 프로퍼티에 직접 적용할 수 있는 애노테이션을 만들 수 있다.

사용 지점 대상을 지정할 때 지원하는 대상 목록은 다음과 같다.

  • property: 프로퍼티 전체. 자바에서 선언된 애노테이션에는 이 사용 지점 대상을 사용할 수 없다.
  • field: 프로퍼티에 의해 생성되는 필드
  • get: 프로퍼티 게터
  • set: 프로퍼티 세터
  • receiver: 확장 함수나 프로퍼티의 수신 개체 파라미터
  • param: 생성자 파라미터
  • setparam: 세터 파라미터
  • delegate: 위임 프로퍼티의 위임 인스턴스를 담아둔 필드
  • file: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스

file 대상을 사용하는 애노테이션은 package 선언 앞에서 파일의 최상위 수준에만 적용할 수 있다.

 

자바와 달리 코틀린에서는 애노테이션 인자로 클래스나 함수 선언이나 타입 외에 임의의 식을 허용한다.

가장 흔히 쓰이는 예로는 컴파일러 경고를 무시하기 위한 @Suppress 애노테이션이 있다.

fun test(list: List<*>) {
    @Suppress("UNCHECKED_CAST")
    val strings = list as List<String>
    // ...
}

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

직렬화(serialization)는 객체를 저장장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것이다.

반대로 역직렬화(deserialization)는 텍스트나 이진 형식으로 저장된 데이터로부터 원래의 객체를 만들어낸다.

직렬화에 자주 쓰이는 형식에 JSON이 있다. 자바와 JSON을 변환할 때 자주 쓰이는 라이브러리로 잭슨(Jackson)과 

지슨(GSON)이 있다. 이들 또한 다른 자바 라이브러리들 처럼 코틀린과 완전 호환된다.

 

JSON에는 객체의 타입이 저장되지 않기 때문에 JSON 데이터로부터 인스턴스를 만들려면 타입 인자로 클래스를 명시해야 한다.

위 사진은 객체와 JSON 표현 사이의 동등성 관계를 보여준다.

원시 타입이나 String 타입의 프로퍼티만 직렬화하려는 클래스 안에 들어있지만, 실제 다른 값 객체 클래스나 여러 값으로 이뤄진 컬렉션 타입의 프로퍼티도 들어갈 수 있다.

 

애노테이션을 활용해 객체를 직렬화하거나 역직렬화하는 방법을 제어할 수 있다.

객체를 JSON으로 직렬화할 때 제이키드 라이브러리는 기본적으로 모든 프로퍼티를 직렬화하며 프로퍼티 이름을 키로 사용한다. 애노테이션을 사용하면 이런 동작을 변경할 수 있다.

  • @JsonExclude 애노테이션을 사용하면 직렬화나 역직렬화 시 그 프로퍼티를 무시할 수 있다.
  • @JsonName 애노테이션을 사용하면 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 애노테이션이 지정한 이름을 쓰게 할 수 있다.
data class Person(
    @JsonName("alias") val firstName: String,
    @JsonExclude val age: Int? = null
}

fisrtName 프로퍼티를 JSON으로 저장할 떄 사용하는 키를 변경하기 위해 @JsonName 애노테이션을 사용하고,

age 프로퍼티를 직렬화나 역직렬화에 사용하지 않기 위해 @JsonExclude 애노테이션을 사용한다.

직렬화 대상에서 제외할 age 프로퍼티에는 반드시 디폴트 값을 지정해야만 한다.

지정하지 않았다면 역직렬화 시 Person의 인스턴스를 새로 만들 수 없다.

애노테이션 선언

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

annotation class JsonExclude

일반 클래스 선언처럼 보이기도 하지만, class 키워드 앞에 annotation이라는 변경자가 붙어있다는 차이점이 있다.

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

따라서 애노테이션 클래스에 본문을 정의하지 못하도록 컴파일러가 막는다.

 

파라미터가 있는 애노테이션을 정의하려면 애노테이션 클래스의 주 생성자에 파라미터를 선언해야 한다.

annotation class JsonName(val name: String)

일반 클래스의 주 생성자 선언 구문을 똑같이 사용하지만 애노테이션 클래스에서는 모든 파라미터 앞에 val을 붙여야 한다.

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

애노테이션 클래스에도 애노테이션을 붙일 수 있다.

애노테이션 클래스에 적용할 수 있는 애노테이션을 메타애노테이션(meta-annotataion)이라 한다.

표준 라이브러리에는 몇 가지의 메타애노테이션이 있으며, 컴파일러가 애노테이션을 처리하는 방법을 제어한다.

프레임워크 중에도 메타애노테이션을 제공하는 것이 있다.

여러 의존관계 주입 라이브러리들이 메타애노테이션을 사용해 주입 가능한 타입이 동일한 여러 객체를 식별한다.

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

어떤 클래스를 선언 메타데이터로 참조할 수 있는 기능이 필요할 때 클래스 참조를 파라미터로 하는 애노테이션

클래스를 선언하면 된다.

인터페이스의 인스턴스를 직접 만들 수 없기 때문에 역직렬화 시 어떤 클래스를 사용해 인터페이스를 구현할지를 지정할 수 있어야 한다.

@DeserializeInterface(CompanyImp1::class)와 같이 클래스 참조를 인자로 받는 애노테이션의 정의법은 다음과 같다.

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

KClass는 자바 java.lang.Class 타입과 같은 역할을 하는 코틀린 타입이다.

코틀린 클래스에 대한 참조를 저장할 때 KClass 타입을 사용하며, KClass의 타입 파라미터는 이 KClass의 인스턴스가 가리키는 코틀린 타입을 지정한다.

ex) CompanyImp1::class의 타입은 KClass<CompanyImp1>이며, 이 타입은 DeserializeInterface의 파라미터 타입인

KClass<out Any>의 하위 타입이다.

 

KClass의 타입 파라미터를 쓸 떄 out 변경자 없이 KClass<Any>라고 쓰면 Deserialize-Interface에게 CompanyImp1::class를 인자로 넘길 수 없고 오직 Any::class 만 넘길 수 있다.

반면 out 키워드가 잇으면 모든 코틀린 타입 T에 대해 KClass<T>가 KClass<out Any>의 하위 타입이 된다.

따라서 Deserialize Interface의 인자로 Any뿐 아니라 Any를 확장하는 모든 클래스에 대한 참조를 전달할 수 있다.

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

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

이런 기본 동작을 변경하고 싶으면 값을 직렬화하는 로직을 직접 제공하면 된다.

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

interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any?
    fun fromJsonValue(jsonValue: Any?): T
}

이 직렬화 로직을 Person 클래스에 적용하는 방법을 보자.

data class Person(
    val name: String,
    @CustomSerializer(DateSerializer::class) val birthDate: Date

ValueSerializer 클래스는 제네릭 클래스이므로 타입 파라미터가 있다.

따라서 ValueSerializer 타입을 참조하려면 항상 타입 인자를 제공해야 한다.

하지만 이 애노테이션이 어떤 타입에 대해 쓰일지 알 수 없으므로 스타 프로젝션을 사용할 수 있다.

annotation class CustomSerializer(
    val serializerClass: KClass<out ValueSerializer<*>>
}

 

클래스를 애노테이션 인자로 받아야 할 때 같은 패턴을 사용할 수 있다.

클래스를 인자로 받을 때 애노테이션 파라미터 타입에 KClass<out 허용할 클래스 이름>을 쓴다.

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

728x90

댓글