어노테이션
어노테이션은 소스코드에 메타데이터를 첨부하는 수단을 의미한다. 간단하게 주석이라고 생각해도 된다.
@RestController
@RequestMapping("/test")
class TestController
위와 같이 `@`와 함께 붙어있는 코드를 어노테이션이라고 하며, 보통 Spring 프레임워크를 사용하면 `@Controller`, `@Service`, `@Repository` 이런 어노테이션을 많이 사용하기 때문에 익숙할 것이다.
어노테이션 선언 및 사용
annotation class Annotation1
annotation class Annotation2
@Annotation1
class Class1
@[Annotation1 Annotation2]
class Class2
자바에서는 `@interface`를 통해 어노테이션을 선언할 수 있었지만, Kotlin에서는 `annotation class`를 통해 선언할 수 있다. 사용 방법은 Java와 동일하게 `@`를 통해 사용할 수 있다. 단 여러 개의 어노테이션을 사용하고 싶을 경우, `@[]` 을 통해 한번에 사용할 수 있다. 대괄호 안에 어노테이션 클래스를 작성하면 되고, 컴마가 아닌 공백으로 구분한다.
어노테이션 생성자
어노테이션 클래스도 일반 클래스처럼 생성자를 가질 수 있으며, 프로퍼티같이 선언할 수 있다. (정확히 프로퍼티는 아니다. 생성자의 파라미터라고 표현하는 것이 맞다.) 한번 코틀린 공식 문서에 나와있는 예제 코드를 살펴보고, 특성을 알아보자.
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"))
우선 생성자의 파라미터로 가능한 타입이 제한되어 있다는 점이다.
1. Primitive Type
2. String
3. Classes (KClass, Clazz)
4. Enums
5. 다른 Annotation 타입
6. 위에 나열된 타입들의 배열
생성자의 파라미터로 위의 타입들을 선언할 수 있다. 왜 제한되냐면, 당연히 어노테이션은 런타임 시점에 인스턴스를 생성하는 것이 아니라, 컴파일 시점에 정적으로 값이 결정되기 때문에, 컴파일 시점에 값이 할당될 수 있는 데이터만 선언이 가능하다. (`const val`로 선언 가능한 데이터 타입과 조금 다르긴 하다)
파라미터로 선언할 때는 null이 불가능한 타입으로 선언해야 한다. JVM에서 어노테이션의 속성으로 null을 허용하지 않기 때문이다.
어노테이션 클래스 내부 혹은 생성자를 호출할 때에 다른 어노테이션 타입을 사용할 때에는 `@` 없이 사용할 수 있다.
메타 어노테이션
어노테이션 클래스는 메타 어노테이션을 통해 해당 어노테이션 클래스의 추가 속성을 부여할 수 있다. 어노테이션 클래스에 다는 어노테이션이라고 생각하면 된다.
어노테이션 | 설명 |
`@Target` | 어노테이션을 달 수 있는 요소를 지정한다 |
`@Retention` | 어노테이션이 바이너리로 출력되도록 하는지, 리플렉션이 적용 가능한지 설정한다. |
`@Repeatable` | 반복적으로 어노테이션을 작성할 수 있도록 한다 |
`@MustBeDocumented` | 공용 API의 일부임을 표시하며, API 문서에 표기되어야 함을 의미한다. |
이 중에서 거의 `@Target`과 `@Retention` 어노테이션을 자주 사용하기 때문에, 이 부분만 조금 상세히 살펴보자.
@Target
`Target` 어노테이션은 해당 어노테이션이 어떤 요소에 달릴 수 있는지 지정하는 역할을 한다.
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets: AnnotationTarget)
public enum class AnnotationTarget {
/** Class, interface or object, annotation class is also included */
CLASS,
/** Annotation class only */
ANNOTATION_CLASS,
/** Generic type parameter */
TYPE_PARAMETER,
/** Property */
PROPERTY,
/** Field, including property's backing field */
FIELD,
/** Local variable */
LOCAL_VARIABLE,
/** Value parameter of a function or a constructor */
VALUE_PARAMETER,
/** Constructor only (primary or secondary) */
CONSTRUCTOR,
/** Function (constructors are not included) */
FUNCTION,
/** Property getter only */
PROPERTY_GETTER,
/** Property setter only */
PROPERTY_SETTER,
/** Type usage */
TYPE,
/** Any expression */
EXPRESSION,
/** File */
FILE,
/** Type alias */
@SinceKotlin("1.1")
TYPEALIAS
}
멤버 변수로 `AnnotationTarget` 열거형 배열을 가지고 있다. 이 열거형에 따라서 작성 가능한 요소가 달라지게 된다.
@Target(AnnotationTarget.CLASS)
annotation class Annotation
@Annotation
class Class(
@Annotation val property: String,
)
만약 위와 같이 코드를 작성한다면, `property` 프로퍼티에 작성된 `@Annotation`은 `This annotation is not applicable to target 'value parameter'` 라는 컴파일 오류가 발생한다. 왜냐하면 `@Annotation`은 Class인 요소에만 작성 가능하다고 `@Target`을 통해 설정해두었기 때문이다.
@Retention
위의 표에서 어떤 말인지 잘 이해가 안갈 수 있는데, 쉽게 해당 어노테이션을 어느 범위까지 유지할 것인지 설정한다고 보면 된다. 우선 선언을 살펴보자.
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Retention(val value: AnnotationRetention = AnnotationRetention.RUNTIME)
public enum class AnnotationRetention {
/** Annotation isn't stored in binary output */
SOURCE,
/** Annotation is stored in binary output, but invisible for reflection */
BINARY,
/** Annotation is stored in binary output and visible for reflection (default retention) */
RUNTIME
}
`AnnotationRetention`을 프로퍼티로 가지고 있으며, 이 열거형에 따라 어노테이션의 사용 범위가 제한된다.
`SOURCE`인 경우 빌드 시 바이너리 코드에 포함되지 않는다. 따라서 컴파일 시점에만 해당 어노테이션을 사용할 수 있게 된다.
`BINARY`는 빌드 시 바이너리 코드에 포함시키지만 리플렉션을 통해 동적으로 어노테이션에 접근할 수 없다. 추후에 설명하겠지만, 어노테이션은 리플렉션과 함께 활용했을 때 용도가 다양해지기 때문에 리플렉션과 어노테이션을 같이 사용하는 경우가 많다.
`RUNTIME`은 바이너리 코드에도 포함되며, 리플렉션도 사용할 수 있다.
use-site target
Koltin에서 어노테이션을 사용할 때, Kotlin의 언어 특성 상 이 어노테이션을 사용하는 요소가 어느 요소인지 명시적으로 지정해야 할 때가 있다. 한번 아래 상황을 보자.
annotation class Annotation()
class Class(
@Annotation var prop: String,
)
위와 같은 상황에서 `@Annotation`은 `prop`이라는 프로퍼티, 프로퍼티의 게터, 세터, 생성자의 파라미터에 사용됐는지 알 수 없다. 또한 이런 경우 프로퍼티 세터에 이 어노테이션을 사용하고 싶었는데, 프로퍼티에 사용되어 의도치않게 작동할 수도 있다. 이럴 경우에는 use-site target을 통해 명시적으로 지정하는 것이 좋다. `:`을 통해 use-site target을 사용할 수 있다.
class Class(
@set:Annotation var prop: String,
)
지정되는 요소의 기준은 Kotlin 언어 특성 상, Java 코드로 컴파일되어 실행되기에 변환되는 Java의 요소를 기준으로 지정할 수 있다. 아래는 Kotlin에서 지원하는 use-site target을 정리해보았다.
이름 | 설명 |
file | 클래스가 생성된 파일 |
property | 프로퍼티 |
field | 프로퍼티에 의해 생성된 backing field에 위치 |
get | 프로퍼티 getter |
set | 프로퍼티 setter |
receiver | 확장함수 혹은 확장프로퍼티의 수신객체 |
param | 생성자 파라미터 |
setparam | 프로퍼티 setter 파라미터 |
delegate | 위임 프로퍼티에 의해 생성된 위임 인스턴스를 저장하는 필드(참고) |