kotlin

Kotlin 애노테이션과 리플렉션

임베디드 친구 2024. 12. 18. 09:56
728x90
반응형

Kotlin 애노테이션과 리플렉션

Kotlin을 사용하다 보면 애노테이션과 리플렉션을 활용해 프로그램의 유연성과 확장성을 높일 수 있는 다양한 기회를 만날 수 있습니다. 오늘은 Kotlin에서 애노테이션과 리플렉션의 개념을 이해하고, 실제로 어떻게 사용하는지 살펴보겠습니다. 예제 코드도 함께 제공하니 따라 해보면서 학습하세요.

애노테이션(Annotation)이란?

애노테이션은 코드에 메타데이터를 추가하는 방법입니다. 이러한 메타데이터는 컴파일러나 런타임에서 특정 동작을 수행하는 데 사용될 수 있습니다. 간단히 말해 애노테이션은 코드에 추가적인 정보를 제공하여 컴파일러나 툴이 이를 이해하고 추가적인 처리를 할 수 있게 해줍니다.

Kotlin에서는 Java에서 사용하던 애노테이션을 그대로 사용할 수 있으며, Kotlin 고유의 애노테이션도 만들 수 있습니다.

애노테이션 사용 예시

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExampleAnnotation(val description: String)

@ExampleAnnotation(description = "This is an example class")
class AnnotatedClass {

    @ExampleAnnotation(description = "This is an example function")
    fun annotatedFunction() {
        println("Hello from annotated function!")
    }
}

위 코드에서 @ExampleAnnotation이라는 사용자 정의 애노테이션을 만들어 클래스와 함수에 적용했습니다. @Target@Retention을 통해 애노테이션의 적용 대상을 지정하고, 유지 기간을 결정할 수 있습니다.

  • @Target: 애노테이션이 적용될 수 있는 대상을 지정합니다. (예: 클래스, 함수 등)
  • @Retention: 애노테이션이 어느 시점까지 유지되는지를 지정합니다. RUNTIME으로 설정하면 런타임에도 애노테이션 정보를 사용할 수 있습니다.

리플렉션(Reflection)이란?

리플렉션은 런타임에 클래스, 메서드, 프로퍼티에 접근하여 정보를 얻거나 조작하는 기능입니다. 이를 통해 런타임에 객체의 메타데이터를 활용하거나 동적으로 동작을 변경할 수 있습니다.

리플렉션은 애노테이션과 함께 사용되면 강력한 도구가 됩니다. 예를 들어, 특정 애노테이션이 적용된 모든 메서드를 찾아서 실행하는 등의 작업이 가능합니다.

Kotlin에서 리플렉션을 사용하려면 kotlin-reflect 라이브러리를 의존성에 추가해야 합니다.

리플렉션 사용 예시

아래 예제는 리플렉션을 사용해 애노테이션이 적용된 클래스와 메서드를 찾아 정보를 출력하는 코드입니다.

import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberFunctions

@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExampleAnnotation(val description: String)

@ExampleAnnotation(description = "This is an example class")
class AnnotatedClass {
    @ExampleAnnotation(description = "This is an example function")
    fun annotatedFunction() {
        println("Hello from annotated function!")
    }

    fun normalFunction() {
        println("Hello from normal function!")
    }
}

fun main() {
    val clazz = AnnotatedClass::class

    // 클래스에 적용된 애노테이션 확인
    val classAnnotation = clazz.findAnnotation<ExampleAnnotation>()
    if (classAnnotation != null) {
        println("Class Annotation: ${classAnnotation.description}")
    }

    // 함수들에 적용된 애노테이션 확인
    for (function in clazz.memberFunctions) {
        val functionAnnotation = function.findAnnotation<ExampleAnnotation>()
        if (functionAnnotation != null) {
            println("Function ${function.name} Annotation: ${functionAnnotation.description}")
            function.call(AnnotatedClass())  // 해당 함수 호출
        }
    }
}

설명

  1. clazz 변수에 AnnotatedClass의 KClass 객체를 할당합니다.
  2. findAnnotation 함수를 사용하여 클래스에 적용된 ExampleAnnotation을 찾습니다.
  3. memberFunctions를 사용하여 클래스의 모든 함수를 순회하며, 각 함수에 적용된 애노테이션을 찾습니다.
  4. 애노테이션이 적용된 함수는 function.call()을 사용하여 호출할 수 있습니다.

위 예제에서는 annotatedFunction에만 ExampleAnnotation이 적용되어 있으므로, 해당 함수만 호출됩니다.

애노테이션과 리플렉션의 실용적 활용 예

애노테이션과 리플렉션을 활용하면 다양한 상황에서 유용하게 적용할 수 있습니다. 몇 가지 실용적인 예를 들어 보겠습니다.

1. 데이터 검증

데이터 클래스의 프로퍼티에 애노테이션을 적용하고, 리플렉션을 통해 유효성 검사를 수행할 수 있습니다. 예를 들어, 다음과 같이 특정 프로퍼티가 빈 값이 아니어야 한다는 애노테이션을 만들 수 있습니다.

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotEmpty(val message: String = "This field cannot be empty")

data class User(
    @NotEmpty val name: String,
    val age: Int
)

fun validate(user: User) {
    val properties = User::class.members.filterIsInstance<kotlin.reflect.KProperty<*>>()
    for (property in properties) {
        val notEmpty = property.findAnnotation<NotEmpty>()
        if (notEmpty != null) {
            val value = property.getter.call(user)
            if (value is String && value.isEmpty()) {
                println("${property.name} - ${notEmpty.message}")
            }
        }
    }
}

fun main() {
    val user = User(name = "", age = 25)
    validate(user)
}

위 코드에서는 @NotEmpty 애노테이션을 사용하여 name 필드가 빈 문자열일 수 없도록 유효성 검사를 수행합니다.

2. REST API 핸들러 자동 등록

애노테이션을 사용하여 특정 함수가 REST API 핸들러임을 표시하고, 리플렉션을 통해 이러한 핸들러를 자동으로 등록할 수 있습니다. 예를 들어, @GetMapping과 같은 애노테이션을 사용해 특정 URL과 매핑되는 핸들러를 정의할 수 있습니다.

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GetMapping(val path: String)

class ApiController {
    @GetMapping("/hello")
    fun hello() {
        println("Hello, World!")
    }
}

fun main() {
    val controller = ApiController::class
    for (function in controller.memberFunctions) {
        val getMapping = function.findAnnotation<GetMapping>()
        if (getMapping != null) {
            println("Mapping found for path: ${getMapping.path}")
            function.call(ApiController())
        }
    }
}

이와 같은 방식으로 간단한 웹 서버를 구현하거나 라우트를 자동으로 등록하는 기능을 손쉽게 만들 수 있습니다.

결론

애노테이션과 리플렉션은 Kotlin을 더욱 유연하고 동적으로 만들어 주는 도구입니다. 애노테이션을 통해 메타데이터를 추가하고, 리플렉션을 통해 이러한 메타데이터를 활용하면 다양한 자동화와 동적 처리를 수행할 수 있습니다. 오늘 소개한 예제를 직접 따라 해보면서 애노테이션과 리플렉션의 강력함을 체험해 보세요.

리플렉션은 특히 런타임에 객체의 구조를 탐색하거나 동적 처리를 해야 할 때 매우 유용합니다. 하지만 남용할 경우 코드가 복잡해지고 성능 저하를 초래할 수 있으므로 필요한 경우에만 적절히 사용하는 것이 좋습니다.

728x90
반응형