[Kotlin] 애노테이션 - 1 (적용과 사용 지점 대상)
오랜만에 Kotlin in Action 책을 다시 부수기로 했다. 우테코 첫 방학이기 때문!
이제 kotlin in action 의 10장 애노테이션과 리플렉션, 11장 DSL 만들기를 보면, Kotlin in Action 의 1, 2부를 모두 보게 된다.
애노테이션을 사용하면 라이브러리가 요구하는 의미를 클래스에게 부여할 수 있다.
코틀린에서 애노테이션을 사용하는 문법은 자바와 똑같지만, 애노테이션을 선언할 때 사용하는 문법은 자바와 약간 다르다.
애노테이션 선언 & 적용
메타데이터를 선언에 추가하면 애노테이션 프로세서가 컴파일 타임/ 런타임에 적절한 처리를 해준다.
메타데이터가 뭔데?
메타데이터(metadata)는 데이터에 대한 데이터라는 의미를 가지며, 특정 정보의 구조, 내용, 관계 등을 설명하는 정보이다.
코드의 일부분이나 프로그램의 동작 방식에 대한 데이터를 기술하는 데 사용된다.
메타데이터는 데이터 자체가 아니라 데이터를 설명하고 구성하는 정보를 제공하기 때문에, 이를 통해 데이터를 이해하고, 관리하며, 효율적으로 사용할 수 있다.
프로그래밍 언어에서 애노테이션(annotations)을 통해 메타데이터를 코드에 추가할 수 있다.
애노테이션은 컴파일러, 런타임 환경, 프레임워크, 라이브러리 등에 추가적인 정보를 제공하여, 코드의 특정 부분이 어떻게 처리되어야 하는지를 명시할 수 있다.
예를 들어, Java와 Kotlin에서는 애노테이션을 사용해 메소드 오버라이딩, 경고 억제, nullability 체크, 리플렉션 등의 기능을 제어하거나, 특정 라이브러리와 프레임워크에서 요구하는 규칙을 코드에 명시하는 데 사용된다.
애노테이션 적용
애노테이션은 `@` 과 애노테이션 이름을 이루어진다.
익숙한 애노테이션을 살펴보자.
@Test
지금까지 테스트 할 때 자주 사용한 JUnit 프레임워크를 사용할 때 아래처럼 사용했다.
@Test
fun testComponentN() {
val client = Client("sh1mj1", 4122)
assertTrue { client.component1() == "sh1mj1" }
assertTrue { client.component2() == 4122 }
}
테스트 메서드 앞에 `@Test` 애노테이션을 붙인다.
`@Test` 애노테이션을 사용해 제이유닛 프레임워크에게 이 메서드를 테스트로 호출하라고 지시한다.
@Deprecated
자바와 코틀린에서 `@Deprecated` 의 의미는 같다.
코틀린에서는 `replaceWith` 파라미터를 통해 옛 버전을 대신할 수 있는 패턴을 제시할 수 있다.
API 사용자는 이를 통해 더 쉽게 새 버전으로 변경할 수 있다.
위와 같은 `remove` 함수 선언이 있다면 인텔리J 아이디어는 `remove` 를 호출하는 코드에 대해 경고 메시지를 표시해주고, 자동으로 그 코드를 새 API 버전에 맞는 코드로 바꿔주는 quick fix 도 제시해 준다.
애노테이션 인자
애노테이션 인자로 아래 요소들을 넣을 수 있다.
원시 타입의 값 | 문자열 | enum | 클래스 참조 | 다른 애노테이션 클래스 |
위 모든 것들로 이루어진 배열 |
- 클래스를 애노테이션 인자로 지정할 때는 `@MyAnnotation(MyClass::class)`처럼 `::class`를 클래스 이름 뒤에 넣어야 한다.
- 다른 애노테이션을 인자로 지정할 때는 인자로 들어가는 애노테이션의 이름 앞에 `@`를 넣지 않아야 한다.
예를 들어 방금 살펴본 예제의 `ReplaceWith`은 애노테이션이지만, `Deprecated`애노테이션의 인자로 들어가므로 `ReplaceWith`앞에 `@`를 사용하지 않는다. - 배열을 인자로 지정하려면 `@RequestMapping(path = arraryOf("/foo", "/bar"))`처럼 `arrayOf` 함수를 사용한다.
자바에서 선언한 애노테이션 클래스를 사용한다면 `value`라는 이름의 파라미터가 필요에 따라 자동으로 가변 길이 인자로 변환된다.
따라서 이 경우라면 `@JavaAnnotationWithArrayValue("abc", "foo", "bar")`처럼 `arrayOf`함수를 쓰지 않아도 된다.
애노테이션 인자와 컴파일 타임
애노테이션 인자는 컴파일 타임에 무엇인지 알 수 있어야 한다. 따라서 임의의 변수, 프로퍼티를 인자로 지정 할 수는 없다.
프로퍼티를 애노테이션 인자로 사용하려면 그 앞에 `const`변경자를 붙여 상수로 만들어야 한다.
companion object {
private const val TEST_DISPLAY = "test display name"
}
@DisplayName(value = TEST_DISPLAY)
@Test
fun name1() {
TODO("Not yet implemented")
}
@DisplayName(value = "test display name")
@Test
fun name2() {
TODO("Not yet implemented")
}
private val foo: String = "arbitrary display name"
@DisplayName(value = foo) // [COMPILE ERROR] An annotation argument must be a compile-time constant
@Test
fun name3() {
TODO("Not yet implemented")
}
위 코드를 살펴 보면 `name3` 이라는 메소드에서 애노테이션 `@DisplayName` 의 `value`를 변수로 넣어서 컴파일 에러가 발생 한다.
애노테이션 대상
코틀린 프로퍼티는 기본적으로 자바 필드와 접근자 메서드 선언과 대응한다.
주 생성자에서 프로퍼티를 선언하면 이 외에 자바 생성자 파라미터와도 대응이 된다.
이렇게 코틀린 소스코드에서 한 가지 선언의 컴파일 한 결과가 여러 자바 선언에 대응하는 경우가 자주 있다.
따라서 애너테이션을 붙일 때 이런 요소 중 어떤 요소에 애노테이션을 붙일지 표시 할 필요가 있다.
use-site target(사용 지점 대상)
사용 지점 대상 선언으로 애노테이션을 붙일 요소를 정할 수 있다.
사용 지점 대상은 `@`기호와 애노테이션 이름 사이에 붙으며, 애노테이션 이름과는 콜론(`:`)으로 분리된다.
JUnit 에서는 각 테스트 메서드 앞에 그 메서드를 실행하기 위한 규칙을 지정할 . 수있다.
예를 들어 `TemporaryFolder`라는 규칙을 사용하면 메서드가 끝나면 삭제될 임시 파일과 폴더를 만들 수 있다.
규칙을 지정하려면 공개(public) 필드나 메서드 앞에 `@Rule` 을 붙여아 한다.
코틀린 테스트 클래스의 `folder` 프로퍼티 앞에 `@Rule` 을 붙이면 JUnit Warning 이 발생한다.
`@Rule`은 필드에 적용되지만 코틀린의 `field`는 기본적으로 비공개이기 때문에 이러한 예외가 생긴다.
`@Rule` 애노테이션을 정확한 대상에 적용하려면 아래처럼 `@get:Rule`을 사용해야 한다.
import org.junit.Rule
import org.junit.rules.TemporaryFolder
class HasTempFolderTest{
@get:Rule // 필드가 아닌 게터에 애노테이션이 붙는다
val folder = TemporaryFolder()
fun testUsingTempFolder() {
val createdFile = folder.newFile("myfile.txt")
val createdFolder = folder.newFolder("subfolder")
// ...
}
}
사용 지점 대상을 프로퍼티 getter 에 적용한 것이다.
자바에 선언된 애노테이션을 사용해서 프로퍼티에 애노테이션을 붙이면, 기본적으로 프로퍼티의 필드에 그 애노테이션이 붙는다.
하지만 코틀린으로 애노테이션을 선언하면 프로퍼티에 직접 적용할 수 있는 애노테이션을 만들 수 있다.
사용 지점 대상 지원 목록
사용 지점 대상을 지정할 때 지원하는 대상은 아래와 같다.
- `property`: 프로퍼티 전체. 자바에서 선언된 애노테이션에서는 이 사용 지점 대상을 사용할 수 없다.
- `field`: 프로퍼티에 의해 생성되는 backing field
class MyClass {
@field:JvmField
var name: String? = null
}
- `get`/`set`: 프로퍼티 게터/세터
// get 사용 예
class MyPropertyClass {
var counter: Int = 0
@get:Synchronized get
@set:Synchronized set
}
- `param`: 생성자 파라미터
class Person(@param:NotNull var name: String)
- `setparam`: 세터 파라미터
class MyPropertyExample {
var name: String = "default"
set(@setparam:NotNull value) {
field = value
}
}
- `receiver`: 확장 함수나 프로퍼티의 수신 객체 파라미터(자주 사용하지 않음)
annotation class Special
class MyExample
@Special
fun @receiver:Special MyExample.myFunction() {
println("This function is special.")
}
- `delegate`: 위임 프로퍼티의 위임 인스턴스를 담아둔 field (명시적으로 사용되는 경우가 거의 없음.)
- `file`: 파일 안에 선언된 최상위 함수와 프로퍼티를 담아두는 클래스
`file` 대상을 사용하는 애노테이션은 pacakage 선언 앞에서 파일의 최상위 수준에만 적용할 수 있다.
흔히 파일의 최상위 선언을 담는 클래스의 이름을 바꿔주는 `@JvmName` 을 자주 사용한다. (@file:JvmName("StringFunctions") 사용했던 글)
코틀린 애노테이션 인자로 클래스/ 함수 선언/ 타입 외에 임의의 식 허용
코틀린에서는 애노테이션 인자로 클래스/ 함수 선언/ 타입 외에 임의의 식을 허용한다.
흔히 사용하는 예시로 `@Suppress` 애노테이션이 있다. 컴파일러 경고를 무시하기 위해 사용한다.
자바 API 를 애노테이션으로 제어하기
코틀린은
- 코틀린으로 선언한 내용을 자바 바이트코드로 컴파일하는 방법
- 코틀린 선언을 자바에 노출하는 방법
을 제어하기 위한 애노테이션을 많이 제공한다.
- `@JvmName`: 코틀린 선언이 만들어내는 자바 필드나 메서드 이름을 변경한다.
// Kotlin
@file:JvmName("UtilityFunctions")
package com.example
fun getFirst(list: List<String>): String = list.firstOrNull() ?: ""
자바에서는 이 함수를 `UtilityFunctions.getFirst(list)`로 호출할 수 있다.
- `@JvmStatic`: 메서드, 객체 선언, 동반 객체에 적용하면 그 요소가 자바 정적 메서드로 노출된다.
// Kotlin
class MyClass {
companion object {
@JvmStatic
fun sayHello() {
println("Hello, World!")
}
}
}
자바에서는 `MyClass.sayHello()`로 호출할 수 있다.
- `@JvmOverloads`: 디폴트 파라미터 값이 있는 함수에 대해 컴파일러가 자동으로 오버로딩한 함수를 생성해준다.
// Kotlin
class User {
@JvmOverloads
fun setInfo(name: String, age: Int = 0) {
println("Name: $name, Age: $age")
}
}
자바에서는 `setInfo("Alice")` 또는 `setInfo("Alice", 30)`으로 호출할 수 있다.
- `@JvmField`: 프로퍼티에 사용하면 게터/세터가 없는 공개된 자바 필드로 프로퍼티를 노출시킨다.
// Kotlin
class Configurations {
@JvmField
val version = 1
}
자바에서는 `new Configurations().version`으로 직접 접근할 수 있다.
다음 글에서 애노테이션을 활용한 JSON 직렬화 제어로 이어집니다!