Kotlin

Kotlin 애노테이션 use-site target

왜 필요한가

Kotlin의 val/var 프로퍼티 하나는 컴파일되면 여러 JVM 요소로 나뉜다. backing field와 getter가 기본이고, var면 setter가 더해지고, 주 생성자 파라미터로 선언했다면 생성자 파라미터까지 붙는다.

class Example(val foo: String)

foo는 필드 하나, getter 하나, 생성자 파라미터 하나로 컴파일된다. 여기에 @Ann val foo: String처럼 타깃 없이 애노테이션을 붙이면, 컴파일러가 이 중 하나를 골라 붙인다. 리플렉션으로 필드를 뒤지는 라이브러리와 getter를 뒤지는 라이브러리가 다른 곳을 본다면, 애노테이션을 붙였는데도 인식되지 않는 상황이 생긴다. use-site target은 이 위치를 명시하는 문법이다.

핵심 정리

타깃 붙는 곳 비고
file 파일 전체 package 선언 위, import 앞에 쓴다
param 주 생성자 파라미터 생성자 파라미터로 선언된 프로퍼티에만
property Kotlin 프로퍼티 자체 JVM에 대응하는 요소가 없다. Java로 선언한 애노테이션에는 못 쓴다
field backing field
get getter
set setter var에만
setparam setter의 파라미터 var에만
delegate 위임 인스턴스를 담는 필드 by 위임 프로퍼티에만
receiver 확장 함수/프로퍼티의 리시버 파라미터 확장 함수·프로퍼티에만
all (2.2~, 실험적) param+field+get(+setparam) 한 번에 -Xannotation-target-all 플래그 필요

예시

class Example(
    @field:Ann val foo: String,
    @get:Ann val bar: String,
    @param:Ann val quux: String
)

Kotlin 2.2.0으로 컴파일해 javap로 확인하면 foo는 private 필드에, bargetBar()에, quux는 생성자 파라미터에 애노테이션이 붙는다. 셋 다 코드에 쓴 위치 그대로다.

by 위임 프로퍼티는 위임 인스턴스를 담는 필드가 따로 생기므로 거기에 붙이려면 delegate를 쓴다.

class Holder {
    @delegate:Ann
    val lazyValue: String by lazy { "z" }
}

lazyValue가 아니라 컴파일러가 만든 lazyValue$delegate 필드(타입 kotlin.Lazy)에 애노테이션이 붙는다.

혼동하기 쉬운 것

타깃을 안 쓰면 어디로 가는가. 컴파일러는 애노테이션의 @Target을 보고 적용 가능한 타깃 중 parampropertyfield 순서로 첫 번째를 고른다. 실제로 확인한 결과:

class Ctor(@Ann val inCtor: String)          // 생성자 파라미터 -> param에만

class Holder {
    @Ann val plain: String = "x"             // 본문 프로퍼티 -> property로
}

inCtor는 생성자 파라미터로 선언됐으므로 param이 적용 가능해 거기서 멈춘다. plain은 생성자 파라미터가 아니라 param이 적용 불가능하고, property가 적용 가능하므로 property로 간다. property는 JVM에 실체가 없어서, 컴파일러는 getPlain$annotations()라는 빈 정적 메서드를 만들어 거기에 애노테이션을 붙인다. 필드가 아니다.

이 우선순위와 합성 메서드가 생기는 이유는 Kotlin 프로퍼티가 여러 JVM 요소로 컴파일되는 이유에서 바이트코드로 다룬다.

propertyfield도 다르다. property는 Kotlin 메타데이터 수준의 개념이라 Kotlin으로 선언한 애노테이션에만 쓸 수 있다. Java로 선언한 애노테이션은 property 타깃 자체가 적용 불가능하므로, 기본 타깃 우선순위에서 자동으로 건너뛰고 field로 간다.

언제 어떤 것을 쓰나

  • 리플렉션으로 필드를 직접 읽는 라이브러리(Gson 등)라면 @field:
  • getter 규칙을 따르는 라이브러리(Bean Validation 등)라면 @get:
  • 생성자 주입 프레임워크라면 대개 기본 타깃(param)만으로 충분하다
  • 위임 프로퍼티에 애노테이션 프로세서를 적용하려면 @delegate:
  • 여러 타깃에 동시에 붙여야 하면 @all:을 검토할 수 있지만, 2.2 기준 실험적 기능이라 옵트인 플래그가 필요하고 안정성 보장이 없다

더 깊이

참고