10.1.1 annotation 적용
자바와 코틀린에서 @Deprecated annotation의 의미는 똑같다. 하지만 코틀린에서는 replaceWith 파라미터를 통해 옛 버전을 대신할 수 있는 패턴을 제시할 수 있고, API 사용자는 그 패턴을 보고 지원이 종료 될 API 기능을 더 쉽게 새 버전으로 포팅할 수 있다. 다음 예제는 @Deprecated annotation에 어떻게 인자를 지정하는지에 대한 코드다.
// 사용 금지를 설명하는 메시지와 대체할 패턴을 지정
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removet(index)"))
fun remove(index: Int) { ... }
annotation에 인자를 넘길 때는 일반 함수와 마찬가지로 괄호 안에 인자를 넣는다. 이런 remove 함수 선언이 있다면 IntelliJ는 remove를 호출하는 코드에 대해 경고 메시지를 표시해줄 뿐 아니라, 자동으로 그 코드를 새로운 버전의 메소드로 바꿔주는 퀵 픽스도 제시해준다.
annotation의 인자로는 원시 타입의 값, 문자열, enum, 클래스 참조, 다른 애노테이션 클래스, 그리고 지금까지 말한 요소들로 이뤄진 배열리 들어갈 수 있다. 애노테이션 인자를 지정하는 문법은 자바와 약간 다르다.
- 클래스를 annotation 인자로 지정할 때는 ::class를 클래스 이름 뒤에 넣어야한다. (ex. @Hello(HelloClass::class)
- 다른 annotation을 인자로 지정할 때는 인자로 들어가는 annotation의 이름 앞에 @을 넣지 않아야한다. 예를 들어, 위의 예제의 ReplaceWith는 annotation이다. 하지만 @Deprecated annotation의 인자로 들어가므로 ReplaceWith 앞에 @를 사용하지 않는다.
- 배열을 인자로 지정하려면 @RequestMapping(path = arrayOf("/foo", "/bar"))처럼 arrayOf를 사용한다. 자바에서 선언한 annotation 클래스를 사용한다면 value라는 이름의 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환된다. 따라서 그런 경우에는 @JavaAnnotationWithArrayValue("abc", "foo", "bar")처럼 arrayOf를 쓰지 않아도 된다.
annotation 인자를 컴파일 시점에 알 수 있어야한다. 따라서 임의의 프로퍼티를 인자로 지정할 수는 없다. 프로퍼티를 annotation 인자로 사용하려면 그 앞에 const 변경자를 붙여야한다. 컴파일러는 const가 붙은 프로퍼티를 컴파일 시점에 상수로 취급한다. 다음은 JUnit의 @Test annotation에 timeout 파라미터를 사용해 밀리초 단위로 타임아웃 시간을 지정하는 예다.
const val TEST_TIMEOUT = 100L
@Test(timeout = TEST_TIMEOUT) fun testMethod() { ... }
const 프로퍼티는 파일의 맨 위나, obejct 안에 선언해야한다. (3.3.1에 설명)
10.1.2 annotation 대상
사용 지점 대상(use-site target) 선언으로 annotation을 붙일 요소를 정할 수 있다. 사용 지점 대상은 @ 기호와 annotation 이름 사이에 붙으며, annotation 이름과는 콜론(:)으로 분리된다.

이 annotation을 사용하는 예를 살펴보자. JUnit에서는 각 테스트 메소드 앞에 그 메소드를 실행하기 위한 규칙을 지정할 수 있다. 예를 들어 TemporaryFoler라는 규칙을 사용하면 메소드가 끝나면 삭제될 임시 파일과 폴더를 만들 수 있다.
규칙을 지정하려면 public 필드나 메소드 앞에 @Rule을 붙여야 한다. 하지만 코틀린 테스트 클래스의 folder라는 프로퍼티 앞에 @Rule을 붙이면 "The @Rule 'folder' must be public"(@Rule을 지정한 'folder'는 공개 필드여야 함)라는 JUnit 예외가 발생한다. @Rule은 필드에 적용되지만 코틀린의 필드는 기본적으로 비공개이기 때문에 이런 예외가 생긴다. @Rule 애노테이션을 정확한 대상에 적용하려면 다음과 같이 @get:Rule을 사용해야 한다.
class HasTempFoler {
@get:Rule // 프로퍼티가 아니라 게터에 annotation이 붙는다
val folder = TemporaryFolder()
@Test
fun testUsingTempFolder() {
val createdFile = folder.newFile("myFile.txt")
val createdFolder = folder.newFolder("subfolder")
// ...
}
}
자바에 선언된 annotation을 사용해 프로퍼티에 annotation을 붙이는 경우 기본적으로 프로퍼티의 필드에 그 annotation이 붙는다. 하지만 코틀린으로 annotation을 선언하면 프로퍼티에 직접 적용할 수 있는 annotation을 만들 수 있다.
사용 지점 대상을 지정할 때 지원하는 대상 목록은 다음과 같다.
- property: 프로퍼티 전체. 자바에서 선언된 annotation에는 이 사용 지점 대상을 사용할 수 없다.
- field: 프로퍼티에 의해 생성되는 (뒷받침하는) 필드
- get: 프로퍼티 게터
- set: 프로퍼티 세터
- receiver: 확장 함수나 프로퍼티의 수신 객체 파라미터
- param: 생성자 파라미터
- setparam: 세터 파라미터
- delegate: 위임 프로퍼티의 위임 인스턴스를 담아둔 필드
- file: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스
file 대상을 사용하는 annotation은 package 선언 앞에서 파일의 최상위 수준에만 적용할 수 있다. 파일에 흔히 적용하는 annotation으로는 파일에 있는 최상위 선언을 담는 클래스의 이름을 바꿔주는 @JvmName이 있다. @file:JvmName("StringFunctions")라는 예제를 3.2.3절에서 이미 봤다.
자바와 달리 코틀린에서는 annotation 인자로 클래스나 함수 선언이나 타입 외에 임의의 식을 허용한다. 가장 흔히 쓰이는 예로는 컴파일러 경고를 무시하기 위한 @Suppress annotation이 있다. 다음은 안전하지 못한 캐스팅 경고를 무시하는 로컬 변수 선언이다.
fun test(list: List<*>) {
@Suppress("UNCHECKED_CAST")
val strings = list as List<String>
// ...
}
자바 API를 annotation으로 제어하기
코틀린은 코틀린으로 선언한 내용을 자바 바이트코드로 컴파일하는 방법과 코틀린 선언을 자바에 노출하는 방법을 제어하기 위한 annotation을 많이 제공한다. 이런 annotation 중 일부는 자바 언어의 일부 키워드를 대신한다. 예를 들어, @Voliatile과 @Strictfp annotation은 자바의 volatile과 strictfp 키워드를 그대로 대신한다. 다음에 나열한 annotation을 사용하면 코틀린 선언을 자바에 노출시키는 방법을 변경할 수 있다.
- @JvmName은 코틀린 선언이 만들어내는 자바 필드나 메소드 이름을 변경한다.
- @JvmStatic을 메소드, 객체 선언, 동반 객체에 적용하면 그 요소가 자바 정적 메소드로 노출된다.
- @JvmOverloads를 사용하면 default 파라미터 값이 있는 함수에 대해 컴파일러가 자동으로 오버로딩한 함수를 생성해준다.
- @JvmField를 프로퍼티에 사용하면 게터나 세터가 없는 공개된 자바 필드로 프로퍼티를 노출시킨다.
10.1.3 annotation을 활용한 JSON 직렬화 제어
annotation을 사용하는 고전적인 예제로 객체 직렬화 제어를 들 수 있다. 직렬화(serialization)는 객체를 저장장치에 저장하거나 네트워크를 통해 전송하기 위해 텍스트나 이진 형식으로 변환하는 것이다. 반대 과정인 역직렬화(deserialization)는 텍스트나 이진 형식으로 저장된 데이터로부터 원래의 객체를 만들어낸다. 직렬화에 자주 쓰이는 형식으로는 JSON이 있다.
제이키드라는 JSON 직렬화 라이브러리의 소스코드를 파악해보자.
라이브러리를 테스트할 수 있는 간단한 예제로 Person 클래스를 직렬화하고 역직렬화하는 것부터 시작해보자. Person의 인스턴스를 serialize 함수에 전달하면 JSON 문자열을 돌려받는다.
data class Person(val name: String, val age: Int)
>>> val person = Person("Alice", 29)
>>> println(serialize(person))
{"age":29, "name": "Alice"}
예제에서 객체 인스턴스의 JSON 표현은 key/value 쌍으로 이뤄진 객체를 표현한다.
JSON 표현을 다시 객체로 만들려면 deserialize 함수를 호출한다.
>>> val json = """{"name": "Alice", "age": 29}"""
>>> println(deserialize<Person>(json))
Person(name=Alice, age=29)
JSON에는 객체의 타입이 저장되지 않기 때문에 JSON 데이터로부터 인스턴스를 만들려면 타입 인자로 클래스를 명시해야 한다. 여기서는 Person 클래스를 타입 인자로 넘겼다.
annotation을 활용해 객체를 직렬화하거나 역직렬화하는 방법을 제어할 수 있다. 객체를 JSON으로 직렬화할 때 제이키드 라이브러리는 기본적으로 모든 프로퍼티를 직렬화하며 프로퍼티 이름을 키로 사용한다. annotation을 사용하면 이런 동작을 변경할 수 있다.
- @JsonExclude annotation을 사용하면 직렬화나 역직렬화 시 그 프로퍼티를 무시할 수 있다.
- @JsonName annotation을 사용하면 프로퍼티를 표현하는 key/value 쌍의 key를 바꿀 수 있다.
data class Person(
@JsonName("alias") val firstName: String,
@JsonExclude val age: Int? = null
)
10.1.4 annotation 선언
위에서 알아본 제이키드의 annotation을 실제로 구현해보자.
annotation class JsonExclude(val name: String)
일반 클래스의 주 생성자 선언 구문과 동일하지만, annotation 클래스에서는 모든 파라미터 앞에 val을 붙여야만 한다.
10.1.5 meta-annotation: annotation을 처리하는 방법 제어
meta-annotation은 annotation클래스에 적용할 수 있는 annotation이다. 표준 라이브러리에는 몇 가지 meta-annotation이 있으며, 그런 meta-annotation들은 컴파일러가 annotation을 처리하는 방법을 제어한다.
표준 라이브러리에 있는 meta-annotation 중 가장 흔히 쓰이는 meta-annotation은 @Target이다. 제이키드의 @JsonExclude와 @JsonName annotation도 적용 가능 대상을 지정하기 위해 @Target을 사용한다.
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude
meta-annotation을 직접 만들어야 한다면 ANNOTATION_CLASS를 대상으로 지정하면 된다.
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation
@BindingAnnotation
annotation class MyBinding
대상을 PROPERTY로 지정한 annotation을 자바 코드에서 사용할 수는 없다. 자바에서 그런 annotation을 사용하려면 AnnotationTarget.FIELD를 두 번째 대상으로 추가해야한다. 그렇게 하면 annotation을 코틀린 프로퍼티와 자바 필드에 모두 적용할 수 있다.
@Retention
@Retention은 정의 중인 annotation 클래스를 소스 수준에서만 유지할지, .class 파일에 저장할지, 실행 시점에 리플렉션을 사용해 접근할 수 있게 할지를 지정하는 meta-annotation이다. 자바 컴파일러는 기본적으로 annotation을 .class 파일에는 저장하지만 런타임에는 사용할 수 없게 한다. 하지만 대부분의 annotation은 런타임에도 사용할 수 있어야 하므로 코틀린에서는 기본적으로 annotation의 @Retetntion을 RUNTIME으로 지정한다. 따라서 코틀린에서는 별도의 @Retention을 붙일 필요가 없다.
10.1.6 annotation 파라미터로 클래스 사용
어떤 클래스를 선언 메타데이터로 참조할 수 있는 기능이 필요할 때가 있다. 클래스 참조를 파라미터로 하는 annotation 클래스를 선언하면 그런 기능을 사용할 수 있다. 제이키드 라이브러리에 있는 @DeserializeInterface는 인터페이스 타입인 프로퍼티에 대한 역직렬화를 제어할 때 쓰는 annotation이다. 인터페이스의 인스턴스를 직접 만들 수는 없다. 따라서 역직렬화 시 어떤 클래스를 사용해 인터페이스를 구현할지를 지정할 수 있어야 한다.
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 annotation의 인자로 CompanyImpl::class를 넘긴다.
annotation class DeserializeInterface(val targetClass: KClass<out Any>)
KClass는 자바 java.lang.Class 타입과 같은 역할을 하는 코틀린 타입이다. 코틀린 클래스에 대한 참조를 저장할 때 KClass 타입을 사용한다. 10장 뒤쪽에 있는 '리플렉션'절에서 이렇게 저장한 클래스 참조로 어떤 일을 할 수 있는지 살펴본다.
KClass의 타입 파라미터는 이 KClass의 인스턴스가 가리키는 코틀린 타입을 지정한다. 예를 들어 CompanyImpl::class의 타입은 KClass<CompanyImpl>이며, 이 타입은 방금 살펴본 DeserializeInterface의 파라미터 타입인 KClass<out Any>의 하위 타입이다.

KClass의 타입 파라미터를 쓸 때 out 변경자 없이 KClass<Any>라고 쓰면 DeserializeInterface에게 CompanyImpl::class를 인자로 넘길 수 없고, 오직 Any::class만 넘길 수 있다. 반면 out 키워드가 있으면 모든 코틀린 타입 T에 대해 KClass<T>가 KClass<out Any>의 하위 타입이 된다(공변성). 따라서 DeserializeInterface의 인자로 Any뿐 아니라 Any를 확장하는 모든 클래스에 대한 참조를 전달할 수 있다.
10.1.7 annotaiton 파라미터로 제네릭 클래스 받기
기본적으로 제이키드는 원시 타입이 아닌 프로퍼티를 중첩된 객체로 직렬화한다. 이런 기본 동작을 변경하고 싶으면 값을 직렬화하는 로직을 직접 제공하면 된다.
@CustomSerializer annotaiton은 커스텀 직렬화 클래스에 대한 참조를 인자로 받는다. 이 직렬화 클래스는 ValueSerializer 인터페이스를 구현해야만 한다.
interfcae ValueSerializer<T> {
fun toJsonValue(value: T): Any?
fun fromJsonValue(jsonValue: Any?): T
}
예를 들어, 날짜를 직렬화하려면 ValueSerializer<Date>를 구현하는 DateSerializer를 사용할 수 있다(구현 생략).
data class Person(
val anme: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)
이 경우 CustomSerializer는 아래와 같이 구현할 수 있다.
annotaiton class CustomSerializer(
val serializerClass: KClass<out ValueSerializer<*>>
)
약간 어렵지만 다행히 클래스를 annotaiton 인자로 받아야할 때마다 같은 패턴을 사용할 수 있다. 클래스를 인자로 받아야 한다면 annotaiton 파라미터 타입에 KClass<out 허용할 클래스 이름>을 쓴다. 제네릭 클래스를 인자로 받아야 하면 KClass<out 허용할 클래스 이름<*>>처럼 허용할 클래스 이름 뒤에 스타 프로젝션을 덧붙인다.
'Kotlin' 카테고리의 다른 글
[Kotlin] reified 키워드 (0) | 2025.04.03 |
---|---|
[Kotlin in Action] 10.2 리플렉션: 실행 시점에 코틀린 객체 내부 관찰 (0) | 2025.04.03 |
[Kotlin] 스타 프로젝션(*)과 Any의 차이 (0) | 2025.04.03 |
[Kotlin] 코틀린 선언 지점 변성과 자바 와일드카드 비교 (0) | 2025.04.03 |
[Kotlin in Action] 9.3 변성: 제네릭과 하위 타입 (0) | 2025.04.02 |
댓글