애노테이션 적용
자바와 마찬가지 방법으로 애노테이션 사용이 가능하다. 애노테이션을 적용하려는 대상 앞에 @ + 애노테이션 이름을 붙이면 된다. 함수나, 클래스 등 여러 다른 코드 구성 요소에 붙일 수 있다.
class MyTest {
@Test
fun testTrue(){ //@Test 어노테이션을 사용해 JUnit 프레임워크에게 이 메서드를 테스트로 호출하라고 지시한다.
Assert.assertTrue(true)
}
}
@Deprecated("Use removeAt(index) instead", //더 이상 사용하지 않거나 권장되지 않는 메서드에 붙이는 애노테이션
ReplaceWith("removeAt(index)")) //해당 파라미터를 통해 옛 버전을 대신할 수 있는 패턴을 제시가능
fun remove(index:Int){
//logic
}
- JUnit 프레임워크를 사용한다면, 테스트 메서드 앞에 @Test 애노테이션이 붙어야 한다.
- @Deprecated 애노테이션은 자바와 똑같다(사용하지 않는 메서드 앞에 붙인다)
- 애노테이션에 인자를 넘길 때에는 괄호 안에 인자를 넣는다.
- 애노테이션 이자로는 원시 타입의 값, 문자열, enum, 클래스 참조, 다른 애노테이션 클래스, 배열이 들어갈 수 있다.
애노테이션을 지정하는 문법
- 클래스를 애노테이션 인자로 지정할 때는, @MyAnnotation(MyClass::class)처럼 ::class를 클래스 이름 뒤에 넣어야 한다
- 다른 애노테이션을 이자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 @를 넣지 않아야 한다
- 배열을 인자로 지정하려면, @RequestMapping(path=arrayOf("/foo", "/bar")처럼 arrayOf 함수를 사용한다.
- 자바에서 선언한 애노테이션 클래스를 쓴다면, value라는 이름의 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환된다.
- 그런 경우 @JavaAnnotationWithArrayValue(..)처럼 arrayOf 함수를 쓰지 않아도 된다.
const val TEST_TIMEOUT = 100L
@Test(timeout = TEST_TIMEOUT)
fun testMethod(){
//logic
}
- 애노테이션 인자를 컴파일 시점에 알 수 있어야 하므로 임의의 프로퍼티를 인자로 지정할 수 없다.
- 프로퍼티를 애노테이션 인자로 사용하려면 그 앞에 const 변경자로 붙여야 한다.
- 컴파일러는 const가 붙은 프로퍼티를 컴파일 시점에 상수로 취급
- 일반 프로퍼티를 애노테이션 인자로 사용하려 시도하면 Only const val can be used in constant expressions라는 오류 발생
애노테이션 대상
애노테이션을 붙일 때 어떤 요소에 애노테이션을 붙일지 표시할 필요가 있다.
사용 지점 대상 선언으로 애노테이션을 붙일 요소를 정할 수 있다. 사용 지점 대상은 @ 와 애노테이션 이름 사이에 붙으며, 애노테이션 이름과는 콜론(:)으로 분리된다.
//사용지점 대상 //애노테이션 이름
@get : Rule
class HasTempFolder {
@get:Rule //프로퍼티가 아니라 게터에 애노테이션이 붙는다
val folder = TemporaryFolder()
@Test
fun testUsingTempFolder(){
val createdFile = folder.newFile("myfile.txt")
val createdFolder = folder.newFolder("subfolder")
}
}
- 공개 필드나 메서드 앞에 @Rule을 붙이면, 메서드를 실행하기 위한 규칙을 지정할 수 있다.
- TemporaryFolder 같은 규칙을 사용하면 메서드가 끝나면 삭제될 임시 파일과 폴더를 만들 수 있다.
- folder라는 프로퍼티에 @Rule을 붙이면, "The @Rule 'folder' must be public"이라는 예외가 발생한다.
- 코틀린에서는 프로퍼티는 기본적으로 비공개 이므로, @Rule 애노테이션 대상을 지정하기 위해서는 사용지점 대상을 지정해야 한다.
사용 지점 대상으로 지정할 때 지원하는 대상 목록
| property | 프로퍼티 전체, 자바에서 선언된 애노테이션에는 이 사용 지점 대상을 사용할 수 없다 |
| field | 프로퍼티에 의해 생성되는 필드 |
| get | 프로퍼티 게터 |
| set | 프로퍼티 세터 |
| receiver | 확장 함수나 프로퍼티의 수신 객체 파라미터 |
| param | 생성자 파라미터 |
| setparam | 세터 파라미터 |
| delegate | 위임 프로퍼티의 위임 인스턴스를 담아둔 필드 |
| file | 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스 |
- file 대상을 사용하는 애노테이션은 package선언 앞에서 파일의 최상위 수준에만 적용할 수 있다.
- 파일에 흔히 적용하는 애노테이션은 파일에 있는 최상위 선언을 담는 클래스의 이름을 바꿔주는 @JvmName이 있다.
fun test(list: List<*>){
@Suppress("UNCHECKED_CAST")
val strings = list as List<String>
}
- 코틀린에서는 애노테이션 인자로 클래스나 함수 선언이나 타입 외에 임의의 식을 허용한다.(ex) @Suppress
TIP : 자바 API를 애노테이션으로 제어하기
코틀린은 코틀린으로 선언한 내용을 자바 바이트코드로 컴파일하는 방법과 코틀린 선언을 자바에 노출하는 방법을 제어하기 위한 어노테이션을 많이 제공한다. 이런 애노테이션 일부는 자바 언어의 일부 키워드를 대신한다.
| @JvmName | 코틀린 선언이 만들어내는 자바 필드나 메서드 이름을 변경 |
| @JvmStatic | 메서드, 객체 선언, 동반 객체에 적용하면 그 요소가 자바 정적 메서드로 노출 |
| @JvmOverloads | 디폴트 파라미터 값이 있는 함수에 대해 컴파일러가 자동으로 오버로딩한 함수를 생성 |
| @JvmFields | 게터나 세터가 없는 공개된 자바 필드로 프로퍼티를 노출 |
애노테이션을 활용한 JSON 직렬화 제어
직렬화 : 객체를 저장장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것
역직렬화 : 텍스트나, 이진형식으로 저장된 데이터로부터 원래의 객체를 만들어 낸다.
제이키드 : JSON 직렬화를 위한 순수 코틀린 라이브러리
참조 : https://github.com/yole/jkid
GitHub - yole/jkid: JSON serialization/deserialization library for Kotlin data classes
JSON serialization/deserialization library for Kotlin data classes - GitHub - yole/jkid: JSON serialization/deserialization library for Kotlin data classes
github.com
import ru.yole.jkid.deserialization.deserialize
import ru.yole.jkid.serialization.serialize
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val person = Person("Alice", 29)
println(serialize(person))
val json = """{"name": "Alice", "age": 29}"""
println(deserialize<Person>(json))
}
- Person 인스턴스를 serialize 함수에 전달하면 JSON 표현이 담긴 문자열을 돌려받는다.
- JSON 표현을 다시 객체로 만들려면 desrialize 함수를 호출한다.
- JSON 에는 객체 타입이 저장되지 않기 때문에 JSON으로부터 인스턴스를 만들기 위해서는 타입 인자로 클래스를 명시해야 한다.
애노테이션을 활용하면 직렬화, 역직렬화 동작을 변경할 수 있다.
| @JsonExclude | 직렬화나 역직렬화 시 그 프로퍼티를 무시 할 수 있다 |
| @JsonName | 프로퍼티를 표현하는 키/값 쌍의 키로 프로퍼티 이름 대신 애노테이션이 지정한 이름을 쓰게 할 수 있다. |
data class Person(
@JsonName("name") val firstName: String,
@JsonExclude val age: Int)
fun main(args: Array<String>) {
val person = Person("Alice", 29)
println(serialize(person))
val json = """{"alias": "Alice", "age": 29}"""
println(deserialize<Person>(json))
}
- 직렬화 대상에서 제외할 age 프로퍼티에는 반드시 디폴트 값을 지정해주어야 한다
애노테이션 선언
JsonExclude 애노테이션 (파라미터가 없는 애노테이션)
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
- class 앞에 annotation 변경자가 붙어있다
- 애노테이션 클래스는 오직 선언이나 식과 관련 있는 메타데이터의 구조를 정의 하기 때문에 내부에 어떤 코드도 들어 있을 수 없다.
- 컴파일러는 애노테이션 클래스에서 본문을 정의하지 못하게 막는다.
JsonName 애노테이션(파라미터가 있는 애노테이션)
@Target(AnnotationTarget.PROPERTY)
annotation class JsonName(val name: String)
//JAVA
public @interface JsonName{
String value();
}
- 모든 파라미터 앞에 val를 붙여야 한다.
- 자바에서는 value로 사용했던 메서드는 코틀린에서는 프로퍼티를 사용한다.
- 자바에서의 value 메서드는 어떤 애노테이션을 적용할 때 value를 제외한 모든 애트리뷰트에는 이름을 명시해야 한다.
- 자바에서 선언한 애노테이션을 코틀린의 구성요소에 적용할 때는 value를 제외한 모든 인자에 대해 이름 붙인 인자 구문을 사용해야만 한다
- 코틀린도 자바 애노테이션에 정의된 value를 특별하게 취급
애노테이션을 처리하는 방법 제어(메타 애노테이션)
코틀린 애노테이션 클래스에도 애노테이션을 붙일 수 있다. 이를 메타 애노테이션이라고 부른다.
표준 라이브러리에는 몇 가지 메타 애노테이션이 있으며, 그런 메타 애노테이션들은 컴파일러가 애노테이션을 처리하는 방법을 제어한다. 표준 라이브러리에 있는 메타 애노테이션 중 가장 많이 사용되는 @Target을 알아보자(적용 가능 대상을 지정하기 위한 애노테이션)

- @Target 메타 애노테이션은 애노테이션을 적용할 수 있는 요소의 유형을 지정한다.
- 애노테이션 클래스에 구체적인 @Target을 지정하지 않으면 모든 선언에 적용할 수 있다.
- 제이키드 라이브러리는 프로퍼티 애노테이션만 사용하므로 꼭 지정해야 한다.

- 애노테이션이 붙을 수 있는 대상이 정의된 Enum은 AnnotationTarget이다
- 둘 이상의 대상을 한꺼번에 선언할 수 있다.
- 대상을 PROPERTY로 지정한 애노테이션을 자바 코드에서 사용할 수 없다
- 자바에서 그런 애노테이션을 사용하려면 AnnotationTarget.FIELD를 두 번째 대상으로 추가해야 한다.
TIP : @Retention 애노테이션
@Retention은 정의 중인 애노테이션 클래스를 소스 수준에서만 유지할지. class 파일에 저장할지, 실행 시점에 리플렉션을 사용해 접근할 수 있게 할지를 지정하는 메타 애노테이션이다. 자바 컴파일러는 기본적으로 애노테이션을. class파일에는 저장하지만 런타임에는 사용할 수 없게 하지만, 대부분의 애노테이션은 런타임에도 사용할 수 있어야 하므로 코틀린에서는 기본적으로 애노테이션의 @Retention을 RUNTIME으로 지정한다.
애노테이션 파라미터로 클래스 사용
어떤 클래스를 선언 메타데이터로 참조할 수 있는 기능이 필요할 때가 있다. 클래스 참조를 파라미터로 하는 애노테이션 클래스를 선언하면 그런 기능을 사용할 수 있다.
제이키드에서 인터페이스의 인스턴스를 직접 만들 수 없으므로 역직렬화시 어떤 클래스를 사용해 인터페이스를 구현할지를 지정할 수 있어야 한다.
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
)
- 직렬화된 Person 인스턴스를 역직렬화하는 과정에서 Company 프로퍼티를 표현하는 JSON을 읽으면 제이키드가 그 프로퍼티 값에 해당하는 JSON을 역직렬화 하면서 CompanyImpl의 인스턴스를 만들어서 Person 인스턴스의 company 프로퍼티에 설정한다
- 역직렬화에 사용할 클래스를 지정하기 위해 @DeserializeInterface 애노테이션 인자로 CompanyImpl::class를 넘긴다.

- KClass는 자바 java.lang.class 타입과 같은 역할을 하는 코틀린 타입이다.(코틀린 클래스에 대한 참조를 저장할 때 KClass를 사용)
- KClass 타입 파라미터는 KClass의 인스턴스가 가리키는 코틀린 타입을 지정한다.
- CompanyImpl::class의 타입은 KClass<CompanyImpl>이며, 이 타입은 KClass<out Any>의 하위 타입이다.
- KClass의 타입 파라미터를 쓸 때 out 변경자 없이 KClass<Any> 라고 쓰면, DeserializeInterface에게 CompanyImpl::class를 인자로 넘길 수 없고, 오직 Any::class만 넘길 수 있다.
- 반면 out 키워드가 있으면 모든 코틀린 타입 T에 대해 KClass<T>가 KClass<out Any>의 하위 타입이 된다(공변성)
- DeserializeInterface의 인자로 Any뿐 아니라 Any를 확장하는 모든 클래스에 참조 전달 가능
애노테이션 파라미터로 제네릭 클래스 받기
기본적으로 제이키드는 원시 타입이 아닌 프로퍼티를 중첩된 객체로 직렬화 하는데 기본 동작을 변경하고 싶다면 값을 직렬화하는 로직을 직접 제공하면 된다. @CustomSerializer 애노테이션은 커스텀 직렬화 클래스에 대한 참조를 인자로 받고, 이클래스는 ValueSerializer 인터페이스를 구현해야만 한다.

data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
- 날씨를 직렬화 한다.

- ValueSerializer 클래스는 제네릭 클래스라 타입 파라미터가 있다. 따라서 ValueSerializer를 참조하려면 항상 타입 인자를 제공해야 한다.
- 이 애노테이션이 어떤 타입에 대해 쓰일지 알 수 없으므로 스타 프로젝션을 사용할 수 있다.
- out ValueSerializer<*> : DateSerializer::class는 올바른 인자로 받아들이지만 Data::class는 거부한다.
- out : ValueSerializer::class뿐 아니라 ValueSerializer를 구현하는 모든 클래스를 받아들인다.
- <*> : ValueSerializer를 사용해 어떤 타입의 값이든 직렬화 할 수 있게 허용한다.
클래스를 인자로 받아야 하면, 애노테이션 파라미터 타입에 KClass<out 허용할 클래스 이름>을 쓴다.
제네릭 클래스를 인자로 받아야 하면, KClass<out 허용할 클래스 이름<*>> 처럼 허용할 클래스 이름 뒤에 스타 프로젝션을 덧붙인다.
'KotlinInAction > 애노테이션과 리플렉션' 카테고리의 다른 글
| 리플렉션: 실행 시점에 코틀린 객체 내부 관찰 2 (0) | 2022.08.17 |
|---|---|
| 리플렉션: 실행 시점에 코틀린 객체 내부 관찰 (0) | 2022.08.16 |