본문 바로가기

Kotlin

공식 문서로 배우는 코틀린 - 39. Annotations

서른아홉 번째, 어노테이션입니다.

 

어노테이션은 코드에 메타데이터를 추가하는 방법입니다. 어노테이션을 선언하기 위해서는  클래스 앞에 annotation 수정자를 붙입니다.

annotation class Fancy

 

어노테이션의 추가적인 속성은 다음과 같은 메타 어노테이션을 정의하는 어노테이션에 추가하는 방식으로 지정할 수 있습니다.

  • @Target : 어노테이션이 붙을 수 있는 (클래스, 함수, 프로퍼티, 표현식 같은) 대상의 종류를 지정합니다
  • @Retention : 어노테이션은 컴파일된 클래스 파일 안에 저장되는지 여부와 실행 시간에 리플렉션을 불 수 있는지 여부를 지정합니다(기본 값은 둘 다 true입니다).
  • @Repeatable : 하나의 대상에 여러 번 사용할 수 있게 해 줍니다.
  • @MustBeDocumented : 어노테이션의 공개 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)
    }
}

 

클래스의 주 생성자에 어노테이션을 추가해야 하는 경우에는 생성자 선언에 constructor 키워드를 추가하고 그 앞에 어노테이션을 붙입니다.

class Foo @Inject constructor(dependency: MyDependency) { ... }

 

프로퍼티 접근자에서 어노테이션을 붙일 수 있습니다.

class Foo {
    var x: MyDependency? = null
        @Inject set
}

 

생성자

어노테이션은 매개변수가 있는 생성자를 가질 수 있습니다.

annotation class Special(val why: String)

@Special("example") class Foo {}

 

다음과 같은 매개변수 타입을 사용할 수 있습니다.

  • Java 원시 타입에 대응되는 타입들 (예: Long)
  • 문자열
  • 클래스 (Foo::class)
  • enum
  • 다른 어노테이션
  • 위에 언급한 타입들의 배열

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)를 사용합니다. Java 코드에서 어노테이션과 인수를 일반적으로 접근할 수 있게, Kotlin 컴파일러가 자동으로 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"))
}

 

인스턴스화 관련된 보다 많은 내용은 여기에서 확인할 수 있습니다.

 

람다

어노테이션은 람다에도 사용할 수 있습니다. 어노테이션을 람다에 지정하면 컴파일러에 의해서 생성되는 람다 몸체 내부의 invoke() 메소드에 적용됩니다. 이렇게 람다에 추가되는 어노테이션은 동시성 제어를 위해 어노테이션을 사용하는 Quasar 같은 프레임워크에 유용합니다. 

annotation class Suspendable

val f = @Suspendable { Fiber.sleep(10) }

 

어노테이션 사용 지점 대상 (Annotation use-site targets)

프로퍼티나 주 생성자의 매개변수에 어노테이션을 추가할 때 해당 항목에 대응하는 Java의 항목이 복수일 수 있고, 그래서, 생성된 Java 바이트 코드에서 어노테이션이 붙을 수 있는 지점이 복수일 수 있습니다. 생성된 코드에서 정확히 어느 지점에 어노테이션이 추가돼야 하는지 다음과 같이 지정할 수 있습니다.

class Example(@field:Ann val foo,    // Java 필드에 추가
              @get:Ann val bar,      // Java 게터에 추가
              @param:Ann val quux)   // Java 생성자 매개변수에 추가

 

같은 방식의 구문을 파일 전체를 대상으로 하는 어노테이션에도 사용할 수 있습니다. 파일의 제일 위의 패키지 디렉티브 위나 기본 패키지인 경우  모든 임포트 전에 전체 파일을 대상 어노테이션을 추가할 수 있습니다.

@file:JvmName("Foo")

package org.jetbrains.demo

 

같은 대상에 복수의 어노테이션을 추가하는 경우 대상을 반복해서 표기하지 않고 대괄호 안에 복수의 어노테이션을 표기할 수 있습니다.

class Example {
     @set:[Inject VisibleForTesting]
     var collaborator: Collaborator
}

 

다음은 지정할 수 있는 사용 지점 대상 전체 목록입니다.

  • file
  • property (이 대상으로 지정한 어노테이션은 Java에서 보이지 않습니다)
  • field
  • get (프로퍼티 게터)
  • set (프로퍼티 세터)
  • receiver (확장 함수나 프로퍼티의 수신자 매개변수)
  • param (생성자 매개변수)
  • setparam (프로퍼티 세터 매개변수)
  • delegate (위임 프로퍼티를 위한 대리자 인스턴스를 저장하는 필드)

확장 함수의 수신자 매개변수에 어노테이션을 추가할 때는 다음과 같이 합니다.

fun @receiver:Fancy String.myExtension() { ... }

 

사용 지점 대상을 지정하지 않으면 어노테이션에 붙어 있는 @Target에 근거하여 대상이 선정됩니다. 이렇게 선정되는 대상이 복수인 경우에는 다음과 같은 순서로 먼저 적용 가능한 대상이 선택 됩니다.

  • param
  • property
  • field

 

Java 어노테이션

Java 어노테이션은 Kotlin에서 100% 호환됩니다.

import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*

class Tests {
    // @Rule 어노테이션을 프로퍼티 게터에 적용합니다.
    @get:Rule val tempFolder = TemporaryFolder()

    @Test fun simple() {
        val f = tempFolder.newFile()
        assertEquals(42, getTheAnswer())
    }
}

 

Java의 어노테이션은 매개변수의 순서가 정의되지 않기 때문에, 인수를 넘기는 일반적인 함수 호출 구문을 사용할 수 없습니다. 대신에, 이름 있는 인수 구문을 사용해야 합니다.

// Java
public @interface Ann {
    int intValue();
    String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C

// 위의 어노테이션이 Kotlin 것이라면, 다음과 같은 식으로 정의될 것이기 때문에
// annotation class Ann(val initValue: Int, val stringValue: String)
// @Ann(1, "abc") class C 사용기 가능합니다.

 

어노테이션 매개변수 배열

java의 value value 매개변수가 배열 타입인 경우, Kotlin에서는 vararg 매개변수가 됩니다.

// Java
public @interface AnnWithArrayValue {
    String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C

 

value 외에 다른 매개변수가 배열 타입이면, 배열 리터럴 구문을 사용하거나 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 어노테이션이 대상에 TYPE이 있으면 Java 어노테이션 대상 목록의 java.lang.annotation.ElementType.TYPE_USE에 매핑됩니다. 이는 TYPE_PARAMETER Kotlin 대상이 java.lang.annotation.ElementType.TYPE_PARAMETER에 매핑되는 것도 유사합니다. 이는 안드로이드의 API 버전 26미한에서는 이러한 대상을 가지고 있지 않아 문제가 됩니다.

 

어노테이션 대상이 TYPE과 TYPE_PARAMETER로 생성되는 것을 피하기 위해서는, 새로운 컴파일러 인수인 -Xno-new-new-java-annotation-targets 를 사용합니다.

// 이런 경우 Java로 매핑되는 대상이 각각
// java.lang.annotation.ElementType.TYPE_USE
// java.lang.annotation.ElementType.TYPE_PARAMETER 인데
// 이는 JVM 1.8 이상부터만 존재합니다. 그러므로, 1.8 미만 버전을 사용할 때는
// 컴파일러 옵션을 사용합니다.
@Target(AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy

 

반복 가능 어노테이션

Java와 같이, Kotlin도 반복 가능한 어노테이션을 가지고 있습니다. 이는 하나의 요소에 반복해서 추가할 수 있습니다. 어노테이션을 반복 가능하게 하려면, 어노테이션 선언에 @kotlin.annotation.Repeatable 메타 어노테이션을 추가합니다. 이렇게 하면 Kotlin과 Java 모두에서 반복해서 사용할 수 있습니다. Java의 반복 가능한 어노테이션도 Kotlin에서 사용할 수 있습니다.

 

Java에서는 반복 가능한 어노테이션 선언시 이 어노테이션을 담을 컨테이너 어노테이션을 명시적으로 선언 및 지정해야 합니다. Kotlin 1.6 이전에는 이런 부분이 반영돼 있지 않아서, Kotlin의 반복 가능 어노테이션을 Java에서 사용할 수 없었습니다. 호환성을 위해 Kotlin 1.6부터 Kotlin 컴파일러가 사전에 정의된 이름으로 컨테이너 어노테이션을 자동 생성하고, Java쪽에서 Kotlin에서 정의한 반복 가능 어노테이션을 사용하는 경우 자동 생성된 컨테이너 어노테이션 사용으로 변경하여 바이트 코드를 생성합니다. 아래에 있는 예제 코드를 위해 컴파일러는 반복 가능 어노테이션을 담는(containing) 어노테이션인 @Tag.Container 라는 컨테이너 어노테이션을를 자동 생성합니다.

@Repeatable
annotation class Tag(val name: String)

// 컴파일러가 다음과 같은 형태의 @Tag.Container라는 컨테이너 어노테이션을 생성합니다.
@Repeatable
annotation class Tag(val name: String) {
    @kotlin.jvm.internal.RepeatableContainer
    public annotation class Container(val value: Array<Tag>)
}
// Java에서 Kotlin의 반복 가능 어노테이션 사용
@Tag(name = "foo")
@Tag(name = "bar")
public class JavaAnn {
    public static void main(String[] args) {
        System.out.println("123");
    }
}

// 위의 코드는 다음과 같은 식으로 바이트 코드가 생성됩니다.
import com.example.Tag.Container;

@Container({@Tag(
    name = "bar"
), @Tag(
    name = "baz"
)})
public class JavaAnn {
    public JavaAnn() {
    }

    public static void main(String[] args) {
        System.out.println("123");
    }
}
// Java에서의 반복 가능 어노테이션 사용 예

@Repeatable(TagContainer.class)
@interface Tag {
    String value();
}

// Java에서는 이런식으로 반드시 명시적으로 컨테이너 어노테이션을 선언해야 합니다.
@interface TagContainer {
    Tag[] value();
}

// 이런 식으로 반복해서 사용하면
@Tag("bar")
@Tag("baz")
public class Foo {
    /* ... */
}

// Java 컴파일러는 다음과 같은 식으로 코드를 생성해 줍니다.
@TagContainer({ @Tag("bar"), @Tag("baz") })
public class Foo {
    /* ... */
}

 

* 당연한 얘기일 수도 있겠으나 Kotlin에서 정의한 반복 가능 어노테이션을 Java에서 쓰는 부분이 없으면 컨테이너는 자동 생성하지 않는 쪽으로 최적화 하는 거 같습니다. 이 거는 버전 1.9 기준으로 경험에 의해(바이트 코드 확인) 확인한 부분으로 실제와는 다를 수도 있습니다.

 

@kotlin.jvm.JvmRepeatable 메타 어노테이션을 사용하여 (Java 처럼) 명시적으로 컨테이너 어노테이션을 지정할 수 도 있습니다. 이렇게 하는 경우 자동 생성은 하지 않습니다.

@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)

annotation class Tags(val value: Array<Tag>)

 

Kotlin이나 Java의 반복 가능한 어노테이션을 리플렉션으로 추출하기 위해서는 KAnnotatedElement.findAnnotations() 함수를 사용합니다.

 

보다 자세한 내용은 여기를 참고하시기 바랍니다.