안드로이드 개발하는 kancho입니다.
이번 포스팅에서는 코틀린의 어노테이션(Annotation)에 대해 알아보고자 합니다.
'코틀린 완벽 가이드' 책을 참고하였습니다.
Annotation(어노테이션)
어노테이션은 코틀린 선언에 메타데이터를 엮어서 활용할 수 있게 해 준다.
커스텀 메타 데이터를 정의하고 코드에서 선언, 식, 파일 등의 요소에 엮는 방법을 제공한다.
3가지 어노테이션
- 코틀린과 안드로이드에 내장된 annotation (ex. Deprecated)
- Annotation에 대한 정보를 나타내기 위한 어노테이션인 meta annotation (ex. Target, Retention)
- 커스텀한 annotation
정의
선언의 앞쪽 변경자 위치에 @이 붙은 어노테이션 이름으로 정의한다.
annotation class MyAnnotation
@MyAnnotation // MyAnnotation 어노테이션 정의
fun myAnnotation() { }
Usage
// 컴파일러 경고를 끄는 Suppress 어노테이션
@Suppress("UNCHECKED_CAST")
fun checkList(list: List<Any>) {
val a = (list as List<String>)
}
// 각괄호([])로 여러 어노테이션 사용
@[Synchronized Strictfp]
fun main() { }
// 주 생성자에 사용할 때는 contructor 키워드 사용
class A @MyAnnotation constructor()
자바 vs 코틀린
자바
어노테이션이 인터페이스로 구성된다.
public @interface Annotation { }
코틀린
특별한 종류의 클래스로 구성된다.
일반 클래스와 달리 멤버, 부생성자, 초기화 코드가 불가하다.
nested class, interface, object는 가능하다.
annotation class MyAnnotation {
// error : Members are not allowed in annotation class
init { }
val text = "test"
// ok
companion object {
val objectText = "objectText"
}
}
자바에서는 속성을 추가하기 위해서 파라미터가 없는 메서드 형태로 지정해야 한다.
public @interface MyAnnotation {
String text();
}
어노테이션에 custom 속성을 추가하려면 생성자 파라미터를 사용해야 한다.
// 파라미터는 항상 val이어야 한다
annotation class MyAnnotation(val text: String)
@MyAnnotation("useful info")
fun annotatedFun() { }
// 디폴트 값이나 가변 인자를 사용할 수 있다
annotation class Dependency(val arg: String, val names: String = "Core")
annotation class Component(val name: String = "Core")
@Component("I/O")
class IO
@Component("Log")
@Dependency("I/O")
class Logger
@Component
@Dependency("I/O", "Log")
class Main
// 일반 클래스 방식으로 인스턴스를 만들 수는 없다
// error : Annotation class cannot be instantiated
val ioComponent = Component("IO")
어노테이션 클래스는 상위 타입을 명시할 수도 없고
어노테이션 클래스를 상속하는 클래스를 만들 수 없다.
어노테이션 클래스는 Any 클래스와 빈 Annotation 인터페이스를 자동으로 상속하며
어노테이션 클래스의 공통 상위 타입 역할을 한다.
어노테이션 인자는 컴파일 시에만 평가되므로 인자에 임의의 식을 넣을 수는 없다.
컴파일러는 어노테이션 파라미터로 사용할 수 있는 타입의 종류를 제한한다.
- Primitive Type
- String
- Enum
- other Annotation
- Class Literals
- Array of the typed listed above
JVM에서는 어노테이션 속성에 null을 저장할 수 없으므로 파라미터는 null이 될 수 없다.
다른 어노테이션을 인자로 사용할 때는 @ 접두사를 붙이지 않아도 된다. 일반 생성자 호출처럼 사용한다.
annotation class Dependency(vararg val componentNames: String)
annotation class Component(
val name: String = "Core",
val dependency: Dependency = Dependency()
)
@Component(dependency = Dependency("I/O", "Log"))
class Main
배열 타입에 각괄호([ ])를 사용해 배열을 만들 수 있다.
annotation class Dependency(val componentNames: Array<String>)
@Dependency(["I/O", "Log"])
class Main
클래스 리터럴을 사용
KClass 타입의 리플렉션 객체로 클래스에 대한 표현을 얻을 수 있다.
KClass 타입은 자바의 Class 타입에 해당하는 코틀린 클래스다.
클래스 이름 뒤에 ::class를 붙여서 리터럴을 만든다.
annotation class Dependency(vararg val componentClasses: KClass<*>)
class IO
class Logger
@Dependency(IO::class, Logger::class)
class Main
사용 지점 대상
여러 언어 요소가 함축되어 있는 선언에 대해 어노테이션이 붙을 수 있다.
class Person(val name: String)
val name: String은 Person 클래스의 생성자 파라미터면서
getter가 있는 클래스 프로퍼티, 프로퍼티 값을 저장하기 위한 backing field 선언을 짧게 줄인 코드이다.
이런 요소 각각에 대해 어노테이션을 붙일 수 있다.
코틀린에서는 어노테이션을 사용하는 시점에 어떤 대상에 대해 어노테이션을 붙이는지 지정할 수 있다.
이런 사용 지점 대상은 키워드를 통해 지정한다.
:(콜론)으로 어노테이션 이름과 구분한다.
- property : 프로퍼티 자체를 대상으로 한다.
- field : backing field를 대상으로 한다.
- get : 프로퍼티의 getter를 대상으로 한다.
- set : 프로퍼티의 setter를 대상으로 한다.
- param : 생성자 파라미터를 대상으로 한다.(val/var이 붙은 파라미터)
- setparam : 프로퍼티 setter의 파라미터를 대상으로 한다.(가변 프로퍼티)
- delegate : 위임 객체를 저장하는 필드를 대상으로 한다.
annotation class A
annotation class B
// firstName, lastName 프로퍼티 getter에 대한 어노테이션
class Person(
@get:[A B] // 어노테이션을 [] 구문으로 묶을 수 있다
val firstName: String,
@get:A @get:B
val lastName: String
)
// receiver 사용해 확장 함수나 프로퍼티의 수신객체에 어노테이션 붙임
fun @receiver:A Person.fullName() = "$firstName $lastName"
내장 어노테이션(Built in Annotation)
코틀린은 여러 가지 내장 어노테이션을 제공한다. 컴파일러 수준에서 특별한 의미를 가진다.
상당수 자바의 메타 어노테이션과 비슷한 역할을 한다.
@Retention
어노테이션이 저장되고 유지되는 방식을 제어한다.
디폴트로 코틀린 어노테이션은 RUNTIME으로 유지된다.
// 어노테이션이 바이너리 출력에 저장되고 리플렉션 API로 관찰할 수 있는지 정한다
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Retention(val value: AnnotationRetention = AnnotationRetention.RUNTIME)
AnnotationRetention 이넘 클래스의 3가지 저장 방식을 사용한다.
// binary 출력에 저장되는 방식을 결정한다
public enum class AnnotationRetention {
// 컴파일 시점에만 존재, 바이너리 출력에 저장되지 않음
SOURCE,
// 바이너리 출력에 저장되지만 런타임에 리플렉션 API로 관찰할 수 없다
BINARY,
// 바이너리 출력에 저장되고 런타임에 리플렉션 API로 관찰할 수 있다
RUNTIME
}
// 자바에서는 RetentionPolicy를 사용하고 CLASS가 디폴트다
// 명시적으로 RUNTIME을 지정하지 않으면 리플렉션에서 관찰할 수 없다
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
Retention 어노테이션을 붙이지 않으면 에러가 발생한다.
// error : Expression annotations with retention other than SOURCE are prohibited
@Target(AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
annotation class NeedToRefactor
@Repeatable
Repeatable이 붙은 어노테이션을 같은 언어 요소에 반복 적용할 수 있다.
기본적으로 어노테이션을 아래와 같이 반복 적용할 수 없다.
@Deprecated("Deprecated")
@Deprecated("more Deprecated")
class OldClass
Repeatable을 통해 반복 적용
// 반복할 수 있는 어노테이션을 런타임까지 유지하지 못하기에 유지 시점을 SOURCE로 명시
@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class Author(val name: String)
@Author("Kancho")
@Author("Park")
class Services
@MustBeDocumented
어노테이션을 문서에 꼭 포함시키라는 의미이다.
어노테이션도 공개 API의 일부인 경우 붙인다.
@MustBeDocumented
annotation class A
@Target
어노테이션을 어떤 언어 요소에 붙일 수 있는지 지정한다.
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets: AnnotationTarget)
AnnotationTarget 이넘 클래스
public enum class AnnotationTarget {
// 클래스, 인터페이스, 객체에 붙일 수 있다(annotation class is also included)
CLASS,
// 오직 어노테이션 클래스에만 붙일 수 있다
ANNOTATION_CLASS,
// 제네릭 타입 파라미터에 붙일 수 있다(unsupported yet)
TYPE_PARAMETER,
// 주 생성자의 val/var 프로퍼티를 포함한 프로퍼티(지역 변수 X)
PROPERTY,
// 프로퍼티를 뒷받침하는 필드에 붙일 수 있다
FIELD,
// 지역 변수 (파라미터는 제외)
LOCAL_VARIABLE,
// 생성자, 함수, 프로퍼티 세터의 파라미터
VALUE_PARAMETER,
// 주생성자, 부생성자
CONSTRUCTOR,
// 람다나 익명함수를 포함해, 함수에 붙일 수 있다(생성자, 프로퍼티 접근자 X)
FUNCTION,
// 프로퍼티 게터
PROPERTY_GETTER,
// 프로퍼티 세터
PROPERTY_SETTER,
// 타입 지정에 붙일 수 있다. 변수의 타입이나 함수 파라미터 타입, 반환 타입 등
TYPE,
// 식에 붙일 수 있다
EXPRESSION,
// 파일에 붙일 수 있다
FILE,
// 타입 별명 정의에 붙일 수 있다
@SinceKotlin("1.1")
TYPEALIAS
}
@StrictFp
부동소수점 연산의 정밀도를 제한해 여러 다른 플랫폼 간의 부동소수점 연산 이식성을 높인다.
@Synchronized
어노테이션이 붙은 함수나 프로퍼티 접근자의 본문에 진입하기 전에 monitor를 획득하고 본문 수행 후 모니터를 해제하게 한다.
@Volatile
어노테이션이 붙은 뒷받침하는 필드를 변경한 내용을 즉시 다른 스레드에서 관찰할 수 있게 해 준다.
@Transient
어노테이션이 붙은 필드를 직렬화 메커니즘이 무시한다.
@Suppress
지정한 이름의 컴파일러 경고를 표시하지 않는다.
식이나 파일을 포함하는 모든 대상에 붙일 수 있다.
val strings = listOf<Any>("1", "2", "3")
val numbers = listOf<Any>(1, 2, 3)
val s = @Suppress("UNCHECKED_CAST") (strings as List<String>)[0]
// warning : Unchecked cast: List<Any> to List<Number>
val n = (numbers as List<Number>)[1]
Suppress는 적용된 요소 내부에 있는 모든 코드에 적용된다.
위 코드를 아래와 같이 경고를 제거할 수 있다.
@Suppress("UNCHECKED_CAST")
fun main() {
val strings = listOf<Any>("1", "2", "3")
val numbers = listOf<Any>(1, 2, 3)
val s = (strings as List<String>)[0] // no warning
val n = (numbers as List<Number>)[1] // no warning
}
@Deprecated
사용 금지 예정이라고 선언하는 것이다.
클라이언트 코드에게 이 선언을 사용하지 않는 것을 권장한다.
이 어노테이션을 사용할 때에는 왜 사용 금지 예정인지 알려주고 대신 쓸 수 있는 대안을 알려주는 메시지를 추가하는 것이 일반적이다.
Deprecated 어노테이션 클래스
@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""),
val level: DeprecationLevel = DeprecationLevel.WARNING
)
사용 예시
@Deprecated(
"use readInt() instead",
ReplaceWith("readInt()")
)
fun readNum() = readLine()!!.toInt()
fun readInt(radix: Int = 10) = readLine()!!.toInt(radix)
fun main() {
// 'readNum(): Int' is deprecated. use readInt() instead
val a = readNum()
}
@ReplaceWith
단독으로 사용할 수 없다.
아무 대상도 지원하지 않는다. 다른 어노테이션 내부에서만 사용할 수 있다.
@Target()
@Retention(BINARY)
@MustBeDocumented
public annotation class ReplaceWith(val expression: String, vararg val imports: String)
Deprecated의 마지막 프로퍼티인 DeprecationLevel을 통해 심각성을 지정할 수 있다.
디폴트는 WARNING이다.
public enum class DeprecationLevel {
// 경고를 표시
WARNING,
// 컴파일 오류로 처리
ERROR,
// 접근하지 못하게 막는다
HIDDEN
}
'코틀린' 카테고리의 다른 글
[코틀린] 자바와 코틀린의 상호 운용성 (0) | 2022.12.04 |
---|---|
[코틀린] DSL (도메인 특화 언어) (0) | 2022.11.27 |
[코틀린] 제네릭 (0) | 2022.11.13 |
[코틀린] 상속(Inheritance) (0) | 2022.11.06 |
[코틀린] Inline 클래스 (0) | 2022.10.23 |