오늘도 더 나은 코드를 작성하였습니까?

애노테이션과 리플렉션 본문

Kotlin in Action/코틀린 답게 사용하기

애노테이션과 리플렉션

hik14 2021. 3. 4. 19:06

지금까지는 클래스와 함수를 사용하는 여러 코틀린의 특성을 보았다. 

이들 모두는 정확한 이름을 알고 있어야 호출/사용 할 수 있었다.

 

애노테이션과 리플렉션을 사용한다면, 이런 제약을 벗어나 임의의 클래스를 다룰수있다

 

애노테이션을 이용하면 라이브러리가 요구하는 의미를 클래스에 부여할 수 있다.

리플랙션을 사용하면 실행 시점에 컴파일러 내부구조를 분석할수있다.

 

애노테이션은 적용하기는 쉽다, 하지만 만들기는 어렵다. 특히 애노테이션을 처리하는 코드는 더 어렵다.

 

애노테이션 선언과 적용

메타데이터를 선언에 추가하면 애노테이션을 처리하는 도구가컴파일 시점이나 실행 시점에 적절한 처리를 해준다.

@+name 형태

 

예) Junit

class MyTest{
    @Test fun testTrue() {
        Assert.assertTrue(true)
    }
}

사용금지를 설명하고 대체할 패턴을 제시한다.

@Deprecated("Use removeAt(index) instead", ReplaceWith("removeAt(index)"))
fun remove(index: Int){ ... }

 

애노테이션에 인자를 넘길 때는 일반 함수와 마찬가지로 괄호 안에 인자를 넣어 넘긴다.

remove를 호출하는 코드에 대해 경고 메세지를 제공하고 자동으로 그 코드를 새로운 API버전에 맞는 코드로 바꿔주는 퀵 픽스도 제공한다.

 

애노테이션의 인자에는 원시타입값, 문자열, enum, 클래스참조, 다른 애노테이션 클래스, 앞의 타입들을 원소로 하는 배열이 들어갈 수 있다. 

 

- 클래스를 애노테이션 인자로 지정할때는 @MyAnnotation(MyClass::class)  ::class 붙혀줘야된다.

- 다른 애노테이션을 인자로 지정할때는 이름 앞에 @를 넣지 않는다. 위의 코드에서 ReplaceWith도 애노테이션이다.

- 배열을 인자로 지정하려면, @RequestMapping(path = arrayOf("/foo" , "/bar")) 처럼 arrayOf 함수를 사용한다. 

*자바에서 지정한 애노테이션 클래스를 이용한다면, value라는 이름의 가변 길이 인자로 변환된다, 따라서 @JavaAnnotationWithArrayValue("abc", "foo", "bar")처럼 arrayOf함수를 안써도 된다. 

 

애노테이션 인자를 컴파일 시점에 알 수 있어야 한다. 따라서 임의의 프로퍼티를 인자로 지정할 수는 없다. 프로퍼티를 애노테이션으로 사용하려면, 컴파일러는 const가 붙은 프로퍼티를 컴파일 시점에 상수로 취급한다. 그래서 const를 붙혀줘야된다.

const val TEST_TIMEOUT = 100L
@Test(timeout = TEST_TIMEOUT) fun testMethod(){ ... }

const가 붙은  프로퍼티는 파일의 최상위나 object안에 선언 해야 하며 원시타입 또는 String으로만 초기화가 가능하다.

 

애노테이션의 대상

코틀린 소스코드에서 한 선언을 컴파일한 결과가 여러 자바 선언과 대응 하는 경우가 있다.

이때 코틀린 선언과 대응하는 자바선언에 각각 애노테이션을 붙여야 할 때가 있다.

 

예) 코틀린의 프로퍼티는 자바의 필드+겟터/셋터 메소드와 대응한다.

주 생성자에서 프로퍼티를 선언하면 이런 접근자메소드와 파라미터 외에 자바 생성자 파라미터와 대응된다.

-> 애노테이션을 붙일 때 이런 요소중 어떤 요소에 애노테이션을 붙일지 표시할 필요가 있다.

 

사용 지점 대상 @ 사용지점대상: 이름 형태 

 

예 Junit은 각 테스트 메소드 앞에 메소드를 실행시키는 조건이 있다.

TemporaryFolder라는 규칙을 사용하면 메소드가 끝나면 삭제될 임시 파일과 폴더를 만들 수 있다.

 

@get: Rule  --->@Rule을 getter에 적용시켜라

만약 folder에 @Rule을 적용하면 일반적으로 코틀린 필드는 비공개라 오류가 발생한다.

"The @Rule ''folder" must be public"

class HasTempFolder{
    @get:Rule // folder의 getter에 붙는다.
    val folder = TemporaryFolder()

    @Test
    fun testUsingTempFolder() {
        val createFile = folder.newFile("myfile.txt")
        val createFolder = folder.newFolder("subfolder")
    }
}

 

자바에 선언된 애노테이션을 사용해 프로퍼티(필드+get/set)에 애노테이션을 붙이면 일반적으로 프로퍼티의 필드에 붙는다.

코틀린으로 애노테이션을 선언하면 프로퍼티(필드+get/set)에 직접 적용할 수는 있는 애노테이션을 만들수있다.

 

property - 프로퍼티 전체 (자바에서 선언된 애노테이션 이 사용지점대상  사용못함)

 

field - 프로퍼티의 백킹필드

get - 프로퍼티 겟터

set - 프로퍼티 셋터

 

receiver - 확장함수 또는 프로퍼티의 수신 객체 파라미터

param - 생성자 파라미터

setparam - 셋터 파라미터

 

delegate - 위임 프로퍼티의 위임 인스턴스를 담아둔 필드

file - 파일 내의 최상위 함수와 프로퍼티를 담아둔 클래스.

 

* @file:JvmName("StringFunctions") 처럼 이름 설정.

자바와 달리 코틀린에서는 애노테이션 인자로 클래스나 함수선언이나 타입 외에 임의 식을 허용해준다.

 

예) @Suppress 애노테이션이다. 안전하지 못한 캐스팅의 경고를 무시하는 로컬변수 선언.

fun test(list: List<*>){

    @Suppress("UNCHECKED_CAST")
    val strings = list as List<String>
}

 

***자바 API를 애노테이션으로 제어하기!

 

코틀린은 코틀린으로 선언한 내용을 자바 바이트코드로 컴파일 하는 방법

코틀린 선언을 자바에 노출하는 방법을 제어하기 위한 애노테이션을 제공한다.

 

@JvmName --> 코틀린 선언의 자바 필드 메소드 이름을 변경

 

@JvmStatic --> 메소드, 객체 선언(object { } ), 동반객체(companion object)에 적용하면 그 요소가 자바에서 정적메소드로 이용가능.

 

@JvmOverloads --> 디폴트 파라미터 값이 있는 함수에 대해 컴파일러가 자동으로 오버로딩 함수 생성.

 

@JvmField --> 프로퍼티에  사용하면 게터 세터가 없는 public 자바 필드로 프로퍼티를 노출시킨다.

 

애노테이션을 활용한 JSON 직렬화 제어

직렬화(Serialization) - 객체를 저장장치, 또는 네트워크 통한 전송을 위해 텍스트나 이진 형식으로 변환하는 것이다.

역직렬화는 반대 과정으로 텍스트나 이진형태의 데이터를 원래 객체를 만들어 내는것.

 

data class Person(val name: String, val age: Int)

fun main() {
    
    val person = Person("Alice", 29)
    println(serialize(person))
    // {"age": 29, "name": "Alice"}

    val json = """{"name":"Alice", "age":29}"""
    println(deserialize<Person>(json))
    // Person(name=Alice, age=29)

JSON에는 객체의 타입이 저장되지 않기 때문에 JSON 데이터로부터 인스턴스를 만들려면, 

타입인자로 클래스를 명시해준다!

 

원시및 String타입만  직렬화 하는 클래스에 안에 들어있지만, 실제로는 다른 값 객체 클래스나 여러 값으로 이루어진 컬렉션 탕ㅂ의 프로퍼티도 들어갈수있다.

 

애노테이션을 이용해 제어해보기

 

1. 기본적으로 모든 프로퍼티를 직렬화 시킴.

2. 키 값은 프로퍼티 이름으로함.

 

@JsonExclude ->  직렬화 및 역직렬화에 그 프로퍼티를 무시한다.

@JsonName -> 키로 쓰이는 프로퍼티 일므 대신 애노테이션이 지정한 이름을 사용.

data class Person(
    @JsonName("alias") val name: String,
    @JsonExclude val age: Int)