티스토리 뷰

728x90

20강. 코틀린의 어노테이션

어노테이션이란?

어노테이션을 붙여 개발자에게 의견을 알리거나 특별한 일이 일어나도록 할 수 있음

어노테이션 만드는 방법

annotation class Shape
annotation class Shape(
    val test: String,
    val number: Int,
    val clazz: KClass<*>
)

KClass

코드로 작성한 클래스를 표현하는 클래스

val kClass: KClass<Annotation> = Annotation::class

@Retention

우리의 어노테이션이 저장되고 유지되는 방식을 제어

@Retention(AnnotationRetention.RUNTIME)
annotation class Shape

Retention의 종류

  • SOURCE: 어노테이션이 컴파일때만 존재
  • BINARY: 어노테이션이 런타임때도 있지만 리플렉션을 쓸수 없음
  • RUNTIME(기본값): 어노테이션이 런타임때 존재하고 리플렉션을 쓸수 있음

@Target

어노테이션을 붙이는 위치를 설정

@Target(AnnotationTarget.CLASS)
annotation class Shape

어노테이션 사용 방법

annotation class Shape(
)

@Shape
class Hello { }

@[Shape Shape1]
class Hello { }
annotation class Shape(
    val text: String,
    val number: Int,
)

@Shape("text", 10)
class Hello { }
annotation class Shape(
    val text: Array<String>
)

@Shape(["text1", "text2"])
class Hello { }

위치가 애매한경우

class Hello(@Shape val name: String)

@Shape는 여러가지 위치로 해석 될수 있음

  1. 생성자 파라미터 name
  2. name 프로퍼티
  3. name 필드
  4. name의 getter

위로 인해 아래와같이 정확한 위치를 설정해줘야함

class Hello(@get:Shape val name: String)

@Repeatable

한 언어 요소에 여러번 붙일수 있는 어노테이션

@Repeatable
annotation class Shape(
    val text: Array<String>
)

@Shape(["text1", "text2"])
@Shape(["text3", "text4"])
class Hello { }

21강. 코틀린의 리플렉션

리플렉션을 통한 예제

  • 함수 executeAll(obj: Any)를 만든다
  • obj@Executable 어노테이션을 갖고있으면 obj에서 파라미터가 없고 반환타입이 Unit인 함수를 모두 실행

의존성 추가

  • build.gradle.kts
    runtimeOnly("org.jetbrains.kotlin:kotlin-reflect:1.9.23")

리플렉션 API

KClass

  • KClass: 코틀린에서의 리플렉션 클래스
  • Class: 자바에서의 리플렉션 클래스
    annotation class Reflection
    
fun main() {
    val kClass = Reflection::class
    val instance = Reflection()
    val kClass2 = instance::class

    val kClass3 = Class.forName("com.reflection.Reflection").kotlin

    kClass.java // Class
    kClass.java.kotlin // KClass
 }

cast()

class GoldFish()

fun castToGoldfish(obj: Any) {
    obj as GoldFish
    GoldFish::class.cast(obj)
}

KType

타입을 표현하는 클래스

val kType: KType = GoldFish::class.createType()

KParameter

코틀린의 파라미터를 표현

KTypeParameter

코틀린의 타입 파라미터를 표현

KCallable

호출될수 있는 언어 요소 표현

KFuntion

코틀린에 있는 함수 표현

KProperty

코틀린에 있는 프로퍼티 표현

간단한 리플렉션 사용 예제

class GoldFish(val name: String) {
    fun print() {
        println("${name} 금붕어!")
    }
}

fun main() {
    val goldfish = GoldFish("금붕")
    goldfish::class.members.first { it.name == "print" }.call(goldfish)
}

예제 구현

annotation class Executable

@Executable
class Reflection {
    fun a() {
        println("a 함수입니다.")
    }

    fun b(): Int {
        println("b 함수입니다.")
        return 1
    }
}

fun executeAll(obj: Any) {
    val kClass = obj::class
    if (!kClass.annotations.contains(Executable())) {
        return
    }

    val callableFunc = kClass.members.filterIsInstance<KFunction<*>>()
        .filter { it.returnType == typeOf<Unit>() }
        .filter { it.parameters.size == 1 && it.parameters[0].type == typeOf<Reflection>() }

    callableFunc.forEach { it.call(kClass.constructors.first().call()) }
}

fun main() {
    executeAll(Reflection())
    // a 함수입니다.
}

22강. 리플렉션 활용 - 나만의 DI 컨테이너 만들기

DI 컨테이너

  • 클래스 정보 관리
  • 등록된 클래스 인스턴스화
  • 등록된 클래스끼리 연결

1단계

object ContainerV1 {
    private val registeredClasses = mutableSetOf<KClass<*>>()

    fun register(clazz: KClass<*>) {
        this.registeredClasses.add(clazz)
    }

    fun <T: Any> getInstance(type: KClass<T>): T {
        return registeredClasses.firstOrNull { it == type }
            ?.let { it.constructors.first().call() as T }
            ?: throw IllegalArgumentException("해당 인스턴스를 찾을 수 없습니다.")
    }
}

fun main() {
    ContainerV1.register(AService::class)
    ContainerV1.getInstance(AService::class).print()
}

class AService {
    fun print() {
        println("AService 입니다.")
    }
}

2단계

object ContainerV2 {
    private val registeredClasses = mutableSetOf<KClass<*>>()
    private val cachedInstances = mutableMapOf<KClass<*>, Any>()

    fun register(clazz: KClass<*>) {
        this.registeredClasses.add(clazz)
    }

    fun <T: Any> getInstance(type: KClass<T>): T {
        if (type in cachedInstances) {
            return type.cast(this.cachedInstances[type])
        }

        val instance = registeredClasses.firstOrNull { it == type }
            ?.let { instantiate(it) as T }
            ?: throw IllegalArgumentException("해당 인스턴스를 찾을 수 없습니다.")

        this.cachedInstances[type] = instance

        return instance
    }

    private fun <T: Any> instantiate(clazz: KClass<T>): T {
        val constructor = findUsableConstructor(clazz)
        val params = constructor.parameters
            .map { param -> getInstance(param.type.classifier as KClass<*>) }
            .toTypedArray()

        return constructor.call(*params)
    }

    // clazz의 생성자 중 사용할수 있는 생성자를 사용
    // 생성자에 넣어야할 타입들이 모두 등록된 경우를 의미
    private fun <T: Any> findUsableConstructor(clazz: KClass<T>): KFunction<T> {
        return clazz.constructors.firstOrNull { constructor -> constructor.parameters.isAllRegistered }
            ?: throw IllegalArgumentException("사용할 수 있는 생성자가 존재하지 않습니다.")
    }

    private val List<KParameter>.isAllRegistered: Boolean
        get() = this.all { it.type.classifier in registeredClasses }
}


fun main() {
    ContainerV2.register(AService::class)
    ContainerV2.register(BService::class)

    ContainerV2.getInstance(BService::class).print()
}

class AService {
    fun print() {
        println("AService 입니다.")
    }
}

class BService(
    private val aService: AService
) {
    fun print() {
        println("BService 입니다.")
        this.aService.pri

3단계

의존성 추가

implementation("org.reflections:reflections:0.10.2")
object ContainerV2 {
    private val registeredClasses = mutableSetOf<KClass<*>>()
    private val cachedInstances = mutableMapOf<KClass<*>, Any>()

    fun register(clazz: KClass<*>) {
        this.registeredClasses.add(clazz)
    }

    fun <T: Any> getInstance(type: KClass<T>): T {
        if (type in cachedInstances) {
            return type.cast(this.cachedInstances[type])
        }

        val instance = registeredClasses.firstOrNull { it == type }
            ?.let { instantiate(it) as T }
            ?: throw IllegalArgumentException("해당 인스턴스를 찾을 수 없습니다.")

        this.cachedInstances[type] = instance

        return instance
    }

    private fun <T: Any> instantiate(clazz: KClass<T>): T {
        val constructor = findUsableConstructor(clazz)
        val params = constructor.parameters
            .map { param -> getInstance(param.type.classifier as KClass<*>) }
            .toTypedArray()

        return constructor.call(*params)
    }

    // clazz의 생성자 중 사용할수 있는 생성자를 사용
    // 생성자에 넣어야할 타입들이 모두 등록된 경우를 의미
    private fun <T: Any> findUsableConstructor(clazz: KClass<T>): KFunction<T> {
        return clazz.constructors.firstOrNull { constructor -> constructor.parameters.isAllRegistered }
            ?: throw IllegalArgumentException("사용할 수 있는 생성자가 존재하지 않습니다.")
    }

    private val List<KParameter>.isAllRegistered: Boolean
        get() = this.all { it.type.classifier in registeredClasses }
}

fun start(clazz: KClass<*>) {
    val reflections = Reflections(clazz.packageName)
    val jClasses = reflections.getTypesAnnotatedWith(MyClass::class.java)
    jClasses.forEach { ContainerV2.register(it.kotlin) }
}

private val KClass<*>.packageName: String
    get() {
        val qualifiedName = this.qualifiedName ?: throw IllegalArgumentException("익명 객체 입니다.")
        val hierarchy = qualifiedName.split(".")
        return hierarchy.subList(0, hierar

23강. 리플렉션 활용 - 타입 안전 이종 컨테이너와 슈퍼 타입 토큰

클래스를 제너릭으로 만들지 않고도 타입 안전하게 동물을 가져올 수 있는 방법이 있음

예제

  • 아래 예제에서 KClass<*>가 타입토큰 역할을 함
class TypeSafeCage {
    private val animals: MutableMap<KClass<*>, Animal> = mutableMapOf()

    fun <T: Animal> getOne(type: KClass<T>): T {
        return type.cast(this.animals[type])
    }

    fun <T: Animal> putOne(type: KClass<T>, animal: T) {
        this.animals[type] = animal
    }

    inline fun <reified T: Animal> getOne(): T {
        return this.getOne(T::class)
    }

    inline fun <reified T: Animal> putOne(animal: T) {
        this.putOne(T::class, animal)
    }
}


fun main() {
    val typeSafeCage = TypeSafeCage()
    typeSafeCage.putOne(Carp::class, Carp("잉어"))
    typeSafeCage.putOne(Carp("잉어"))

    val carp = typeSafeCage.getOne(Carp::class)
    val carp2: Carp = typeSafeCage.getOne()
}


위와같은 패턴을 **타입 안전 이종 컨테이너** 패턴이라고 함


타입 안전 이종 컨테이너의 취약점

- 아래와같은 현상은 제네릭 타입 소거가 원인

val cage = TypeSafeCage()
cage.putOne(listOf(GoldFist("금붕어1"), GoldFist("금붕어1")))
val carps: List = cage.getOne() // 금붕어 리스트를 반환받음

슈퍼 타입 토큰

제네릭 타입 정보를 리플렉션으로 알아내기
`List<T>`를 저장하면 `List`와 `T`를 기억함

class SuperTypeSafeCage {
    private val animals: MutableMap<SuperTypeToken<*>, Any> = mutableMapOf()

    fun <T: Any> getOne(token: SuperTypeToken<T>): T {
        return this.animals[token] as T
    }

    fun <T: Any> putOne(token: SuperTypeToken<T>, animal: T) {
        this.animals[token] = animal
    }
}

// SuperTypeToken을 구현한 클래스가 인스턴스화 되자마자
// T정보를 내부 변수에 저장함
abstract class SuperTypeToken<T> {
    val type: KType = this::class.supertypes[0].arguments[0].type!!

    override fun equals(other: Any?): Boolean {
        if (this === other) return true

        other as SuperTypeToken<*>

        return type == other.type
    }

    override fun hashCode(): Int {
        return type.hashCode()
    }

}

fun main() {
    val superTypeToken1 = object : SuperTypeToken<List<GoldFish>>() {}
    val superTypeToken2 = object : SuperTypeToken<List<GoldFish>>() {}
    val superTypeToken3 = object : SuperTypeToken<List<Carp>>() {}
    println(superTypeToken1.equals(superTypeToken2)) // true
    println(superTypeToken1.equals(superTypeToken3)) // false

    val superTypeSafeCage = SuperTypeSafeCage()
    superTypeSafeCage.putOne(superTypeToken1, listOf(GoldFish("금붕어1"), GoldFish("금붕어2")))

//    val result1 = superTypeSafeCage.getOne(superTypeToken3) // error

    val result2 = superTypeSafeCage.getOne(superTypeToken1)
    println(result2)
}

위 내용은 인프런 코틀린 고급편강의를 시청하고 작성했습니다
인프런
 
728x90
댓글