어노테이션
어노테이션
코드에 "메타데이터"를 붙여서 컴파일러나 프레임워크가 그 정보를 읽고 다른 동작을 하게 만드는 도구가 바로 어노테이션이에요. Kotlin에서 어노테이션은 특별한 종류의 클래스로 선언돼요. 어떻게 선언하고, 어디에 붙일 수 있는지, Java와는 어떻게 다른지 하나씩 확인해 볼게요.
출처: Kotlin 공식 문서
본문
어노테이션은 코드의 요소에 메타데이터를 붙이기 위해 사용하는 태그예요. 도구와 프레임워크는 컴파일 시간과 런타임에 이 메타데이터를 처리하고, 그에 따라 여러 가지 동작을 수행해요.
어노테이션을 사용하면 보일러플레이트 코드 생성, 코딩 표준 강제, 문서 작성 같은 일상적인 작업을 단순화하고 자동화할 수 있어요.
나만의 어노테이션 프로세서를 만들고 싶다면 Kotlin Symbol Processing(KSP) API를 사용할 수 있어요.
선언
어노테이션은 특별한 종류의 클래스예요. 어노테이션을 선언하려면 클래스 선언 앞에 annotation 키워드를 붙이면 돼요.
annotation class Fancy
어노테이션의 추가 속성은 메타-어노테이션(meta-annotations)으로 어노테이션 클래스에 붙여서 지정할 수 있어요.
@Target— 이 어노테이션으로 표시할 수 있는 요소의 종류를 지정해요 (클래스, 함수, 속성, 표현식 등).@Retention— 어노테이션이 컴파일된 클래스 파일에 저장되는지, 런타임에 리플렉션으로 보이는지 지정해요 (기본값은 둘 다 true).@Repeatable— 하나의 요소에 같은 어노테이션을 여러 번 사용할 수 있게 해줘요.@MustBeDocumented— 어노테이션이 public API의 일부이며, 생성된 API 문서에 표시되는 클래스나 메서드 시그니처에 포함되어야 함을 지정해요.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy
사용
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
클래스의 주 생성자(primary constructor)에 어노테이션을 붙여야 한다면, 생성자 선언에 constructor 키워드를 추가하고 그 앞에 어노테이션을 붙이면 돼요.
class Foo @Inject constructor(dependency: MyDependency) { ... }
속성 접근자(property accessors)에도 어노테이션을 붙일 수 있어요.
class Foo {
var x: MyDependency? = null
@Inject set
}
생성자
어노테이션은 매개변수를 받는 생성자를 가질 수 있어요.
annotation class Special(val why: String)
@Special("example") class Foo {}
허용되는 매개변수 타입은 다음과 같아요.
- Java 원시 타입에 대응하는 타입 (Int, Long 등)
- 문자열
- 클래스 (
Foo::class) - 열거형 (Enums)
- 다른 어노테이션
- 위 타입들의 배열
어노테이션 매개변수는 nullable 타입이 될 수 없어요. JVM이 null을 어노테이션 속성 값으로 저장하는 것을 지원하지 않기 때문이에요.
어노테이션을 다른 어노테이션의 매개변수로 사용할 때는 이름 앞에 @ 문자를 붙이지 않아요.
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""))
@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))
어노테이션의 인자로 클래스를 지정해야 한다면 Kotlin 클래스(KClass)를 사용해요. Kotlin 컴파일러가 이를 Java 클래스로 자동 변환하므로, Java 코드에서도 어노테이션과 인자를 정상적으로 접근할 수 있어요.
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
인스턴스화
Java에서 어노테이션 타입은 인터페이스의 한 형태라서 구현하고 인스턴스를 사용할 수 있어요. 이 메커니즘의 대안으로, Kotlin은 임의의 코드에서 어노테이션 클래스의 생성자를 호출하고 결과 인스턴스를 동일하게 사용할 수 있게 해줘요.
annotation class InfoMarker(val info: String)
fun processInfo(marker: InfoMarker): Unit = TODO()
fun main(args: Array<String>) {
if (args.isNotEmpty())
processInfo(getAnnotationReflective(args))
else
processInfo(InfoMarker("default"))
}
어노테이션 클래스의 인스턴스화에 대해 더 알고 싶다면 이 KEEP을 참고하세요.
람다
어노테이션은 람다에도 사용할 수 있어요. 람다 본문이 생성되는 invoke() 메서드에 어노테이션이 적용되죠. 동시성 제어에 어노테이션을 사용하는 Quasar 같은 프레임워크에서 유용해요.
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
어노테이션 사용 위치 대상 (use-site targets)
속성이나 주 생성자 매개변수에 어노테이션을 붙일 때, 대응하는 Kotlin 요소에서 여러 Java 요소가 생성되므로 생성된 Java 바이트코드에서 어노테이션이 위치할 수 있는 자리도 여러 곳이 돼요. 어노테이션이 정확히 어떻게 생성될지 지정하려면 다음 문법을 사용해요.
class Example(@field:Ann val foo, // Java 필드에만 어노테이션
@get:Ann val bar, // Java getter에만 어노테이션
@param:Ann val quux) // Java 생성자 매개변수에만 어노테이션
같은 문법으로 전체 파일에 어노테이션을 붙일 수도 있어요. file 대상을 가진 어노테이션을 파일 최상위, 패키지 지시문 앞에 두면 되는데요, 파일이 default 패키지에 있으면 모든 import 앞에 두면 돼요.
@file:JvmName("Foo")
package org.jetbrains.demo
같은 대상을 가진 어노테이션이 여러 개라면, 대상 뒤에 대괄호를 붙여 그 안에 모든 어노테이션을 넣음으로써 대상을 반복하지 않을 수 있어요 (all 메타-대상은 제외).
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
지원되는 사용 위치 대상의 전체 목록은 다음과 같아요.
filefieldproperty(이 대상을 가진 어노테이션은 Java에서 보이지 않아요)get(속성 getter)set(속성 setter)all(속성을 위한 메타-대상)receiver(확장 함수나 속성의 리시버 매개변수)
확장 함수의 리시버 매개변수에 어노테이션을 붙이려면 다음 문법을 사용해요.
fun @receiver:Fancy String.myExtension() { ... }
param(생성자 매개변수)setparam(속성 setter 매개변수)delegate(위임된 속성의 delegate 인스턴스를 저장하는 필드)
사용 위치 대상을 지정하지 않았을 때의 기본값
사용 위치 대상을 지정하지 않으면, 컴파일러는 사용한 어노테이션의 @Target 어노테이션에 따라 대상을 선택해요. 적용 가능한 대상이 여러 개라면 컴파일러는 다음 순서로 그중 하나 이상을 선택해요.
- 생성자 매개변수 대상 (
param) - 속성 대상 (
property) - 필드 대상 (
field) — 적용 가능하고 속성 대상(property)이 적용 불가능할 때
param, property, field 모두 적용 불가능하면 어노테이션은 유효하지 않으므로, 사용 위치 대상을 명시적으로 지정해야 해요.
Jakarta Bean Validation의 @Email 어노테이션을 사용해 볼게요.
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
public @interface Email { }
이 어노테이션으로 다음 예시를 살펴봐요.
data class User(val username: String,
// @Email은 이제 @param:Email @field:Email과 동일
@Email val email: String) {
// @Email은 여전히 @field:Email과 동일
@Email val secondaryEmail: String? = null
}
이 예시에서 @Email 어노테이션은 email 속성에 대해 생성자 매개변수와 필드 대상 모두에 적용돼요. 그 이유는 해당 속성이:
- 주 생성자에 선언됐고
- 커스텀 getter나 setter가 없어서 컴파일러가 backing field를 생성하기 때문이에요.
@Email 어노테이션은 secondaryEmail 속성에 대해 필드 대상에만 적용돼요. 그 이유는 해당 속성이:
- 주 생성자에 선언되지 않았고
- 커스텀 getter나 setter가 없어서 컴파일러가 backing field를 생성하기 때문이에요.
all 메타-대상
all 대상은 같은 어노테이션을 매개변수와 속성 또는 필드에만 적용하는 대신, 대응하는 getter와 setter에도 적용하기 쉽게 해줘요.
구체적으로 all로 표시된 어노테이션은 적용 가능할 경우 다음으로 전파됩니다.
- 속성이 주 생성자에 정의된 경우 생성자 매개변수(
param)로 - 속성 자체(
property)로 - 속성에 backing field가 있는 경우 backing field(
field)로 - getter(
get)로 - 속성이
var로 정의된 경우 setter 매개변수(setparam)로 - 클래스에
@JvmRecord어노테이션이 있는 경우 Java 전용 대상RECORD_COMPONENT로
Jakarta Bean Validation의 @Email 어노테이션을 사용해 보면, 다음과 같이 정의돼 있어요.
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
public @interface Email { }
아래 예시에서 이 @Email 어노테이션은 관련된 모든 대상에 적용돼요.
data class User(
val username: String,
// @Email을 param, field, get에 적용
@all:Email val email: String,
// @Email을 param, field, get, setparam에 적용
@all:Email var name: String,
) {
// @Email을 field와 getter에 적용 (생성자에 없으므로 param은 아님)
@all:Email val secondaryEmail: String? = null
}
all 메타-대상은 주 생성자 안과 밖 어디에서든 모든 속성과 함께 사용할 수 있어요.
제한 사항
all 대상에는 몇 가지 제한이 있어요.
- 타입, 잠재적 확장 리시버, 컨텍스트 리시버, 매개변수에는 어노테이션을 전파하지 않아요.
- 여러 어노테이션과 함께 사용할 수 없어요.
@all:[A B] // 금지, @all:A @all:B를 사용해요
val x: Int = 5
- 위임된 속성(delegated properties)에는 사용할 수 없어요.
Java 어노테이션
Java 어노테이션은 Kotlin과 100% 호환돼요.
import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*
class Tests {
// 속성 getter에 @Rule 어노테이션 적용
@get:Rule val tempFolder = TemporaryFolder()
@Test fun simple() {
val f = tempFolder.newFile()
assertEquals(42, getTheAnswer())
}
}
Java로 작성된 어노테이션의 매개변수 순서는 정의돼 있지 않으므로, 인자 전달에 일반 함수 호출 문법을 사용할 수 없어요. 대신 명명된 인자(named argument) 문법을 사용해야 해요.
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
Java에서와 마찬가지로 특별한 경우가 value 매개변수예요. 그 값은 이름을 명시하지 않고 지정할 수 있죠.
// Java
public @interface AnnWithValue {
String value();
}
// Kotlin
@AnnWithValue("abc") class C
배열을 어노테이션 매개변수로
Java의 value 인자가 배열 타입이면 Kotlin에서는 vararg 매개변수가 돼요.
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
배열 타입인 다른 인자의 경우 배열 리터럴 문법이나 arrayOf(...)를 사용해야 해요.
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
// Kotlin
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C
어노테이션 인스턴스의 속성에 접근
어노테이션 인스턴스의 값은 Kotlin 코드에 속성으로 노출돼요.
// Java
public @interface Ann {
int value();
}
// Kotlin
fun foo(ann: Ann) {
val i = ann.value
}
JVM 1.8+ 어노테이션 대상 미생성 기능
Kotlin 어노테이션이 Kotlin 대상 중 TYPE을 가지면, Java 어노테이션 대상 목록에서 java.lang.annotation.ElementType.TYPE_USE로 매핑돼요. TYPE_PARAMETER Kotlin 대상이 java.lang.annotation.ElementType.TYPE_PARAMETER Java 대상으로 매핑되는 것과 마찬가지죠. 이는 API 레벨 26 미만인 Android 클라이언트에서 문제가 되는데, 해당 API에는 이런 대상이 없거든요.
TYPE_USE와 TYPE_PARAMETER 어노테이션 대상이 생성되는 것을 피하려면 새 컴파일러 인자 -Xno-new-java-annotation-targets를 사용해요.
반복 가능한 어노테이션
Java에서처럼 Kotlin에도 반복 가능한 어노테이션이 있어서, 단일 코드 요소에 여러 번 적용할 수 있어요. 어노테이션을 반복 가능하게 만들려면 선언을 @kotlin.annotation.Repeatable 메타-어노테이션으로 표시하면 돼요. 그러면 Kotlin과 Java에서 모두 반복 가능해져요. Java의 반복 가능한 어노테이션도 Kotlin 쪽에서 지원돼요.
Java에서 사용하는 방식과의 주요 차이는 컨테이너 어노테이션(containing annotation)이 없다는 점이에요. Kotlin 컴파일러가 미리 정의된 이름으로 이를 자동 생성하죠. 아래 예시의 어노테이션에 대해 컴파일러는 @Tag.Container 컨테이너 어노테이션을 생성해요.
@Repeatable
annotation class Tag(val name: String)
// 컴파일러가 @Tag.Container 컨테이너 어노테이션을 생성해요
컨테이너 어노테이션에 커스텀 이름을 지정하려면 @kotlin.jvm.JvmRepeatable 메타-어노테이션을 적용하고 명시적으로 선언한 컨테이너 어노테이션 클래스를 인자로 전달하면 돼요.
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)
annotation class Tags(val value: Array<Tag>)
리플렉션으로 Kotlin이나 Java의 반복 가능한 어노테이션을 추출하려면 KAnnotatedElement.findAnnotations() 함수를 사용해요.
Kotlin의 반복 가능한 어노테이션에 대해 더 알고 싶다면 이 KEEP을 참고하세요.