본문 바로가기

코틀린

[코틀린] Annotation

 

안드로이드 개발하는 kancho입니다.

이번 포스팅에서는 코틀린의 어노테이션(Annotation)에 대해 알아보고자 합니다.

'코틀린 완벽 가이드' 책을 참고하였습니다.

 

 

Annotation(어노테이션)

어노테이션은 코틀린 선언에 메타데이터를 엮어서 활용할 수 있게 해 준다.

커스텀 메타 데이터를 정의하고 코드에서 선언, 식, 파일 등의 요소에 엮는 방법을 제공한다.

 

3가지 어노테이션

  1. 코틀린과 안드로이드에 내장된 annotation (ex. Deprecated)
  2. Annotation에 대한 정보를 나타내기 위한 어노테이션인 meta annotation (ex. Target, Retention) 
  3. 커스텀한 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