kotlin - 리플렉션

조갱·2024년 5월 12일
0

kotlin

목록 보기
11/12

이전에 kotlin - 람다와 멤버참조 에서 한번 언급한적이 있어요.!

리플렉션이란?

리플렉션이란, 런타임 시점에 클래스의 정보를 활용할 수 있는 기법을 의미한다.
더 깊게는 클래스, 메소드, 필드, 어노테이션에 대한 정보들을 알 수 있다.

이를 잘 활용하면 런타임 시점에 클래스 정보를 무궁무진하게 활용할 수 있지만,
런타임 시점에 활용하는 데이터인만큼 단점과 위험성도 분명하게 존재한다.

*심지어는, private 접근제한자에도 접근할 수 있다!!

언제 사용할까?

원리적으로

리플렉션은 일반적인 개발환경에는 사용하지 않는다.
라이브러리나, 프레임워크같이 공통적인(=범용적인) 환경을 개발할 때 사용한다.

그 이유는, 일반적인 개발환경에서는 우리가 클래스의 구조를 알고, 인스턴스를 만들어 활용할 수 있기 때문이다. 라이브러리나 프레임워크같은 공통적인 환경에서는 어떤 클래스와 인스턴스가 들어올지 모르기 때문에, 이 때 리플렉션을 활용한다.

실전에서

가장 대표적으로 백엔드 개발자들이 가장 많이 활용하는 jackson 라이브러리,
자바와 스프링에서 사용하는 어노테이션 등이 리플렉션을 사용한다.

예제코드

예제 클래스 - Person

annotation class TEST()

@TEST
class Person(
    // 주생성자
    val name: String,
    val age: Int,
) {

    // 부생성자
    constructor() : this("Anonymous", 0)

    // 필드, public getter와 private setter
    var nickName: String = ""
        private set

    // public method (파라미터 없음)
    fun info(): String {
        return "${age}${name}($nickName)님"
    }

    // public method (파라미터 1개)
    fun changeNickName(newNickName: String) {
        nickName = newNickName
    }

    // private method (파라미터 없음)
    private fun methodForTest(): String {
        return "TEST : $name"
    }
}

클래스 정보 받아오기 (Kotlin)

코틀린에서는 클래스 뒤에 ::class 를 붙이면 KClass 라는 코틀린의 리플렉션 객체를 얻을 수 있다.

val clazz = Person::class

// 클래스의 생성자 목록을 불러온다
// [fun <init>(): A.B.C.Person, fun <init>(kotlin.String, kotlin.Int): A.B.C.Person]
println(clazz.constructors)

// 클래스 안에 정의된 클래스 목록들을 불러온다.
// []
println(clazz.nestedClasses)

// 패키지를 포함한 클래스명을 불러온다.
// A.B.C.Person
println(clazz.qualifiedName)

// 클래스의 이름을 불러온다.
// Person
println(clazz.simpleName)

// 상위 클래스들을 불러온다.
// [kotlin.Any] (= java의 Object)
println(clazz.supertypes)

// 클래스에 속한 멤버들 (필드 및 메소드)을 불러온다.
// [val A.B.C.Person.age: kotlin.Int, val A.B.C.Person.name: kotlin.String, var A.B.C.Person.nickName: kotlin.String, fun A.B.C.Person.changeNickName(kotlin.String): kotlin.Unit, fun A.B.C.Person.info(): kotlin.String, fun A.B.C.Person.methodForTest(): kotlin.String, fun A.B.C.Person.equals(kotlin.Any?): kotlin.Boolean, fun A.B.C.Person.hashCode(): kotlin.Int, fun A.B.C.Person.toString(): kotlin.String]
println(clazz.members)

// 클래스에 속한 필드들만 불러온다.
// [val A.B.C.Person.age: kotlin.Int, val A.B.C.Person.name: kotlin.String, var A.B.C.Person.nickName: kotlin.String]
println(clazz.memberProperties)

// 클래스에 속한 메소드들만 불러온다.
// [fun A.B.C.Person.changeNickName(kotlin.String): kotlin.Unit, fun A.B.C.Person.info(): kotlin.String, fun A.B.C.Person.methodForTest(): kotlin.String, fun A.B.C.Person.equals(kotlin.Any?): kotlin.Boolean, fun A.B.C.Person.hashCode(): kotlin.Int, fun A.B.C.Person.toString(): kotlin.String]
println(clazz.memberFunctions)

// 클래스에 속한 어노테이션을 불러온다.
// [@A.B.C.TEST()]
println(clazz.annotations)

클래스 정보 받아오기 (Java)

KClass 에서 제공하지 않는 기능도 있다. 코틀린은 java와 99% 호환되니, java의 Class타입으로 리플렉션을 할 수도 있다.

사용법은 간단하게, 클래스명 뒤에 ::class.java 를 써주면 된다.

위 사진과 같이 java의 Class객체를 사용할 수 있다.

private 인스턴스에 값 할당하기 (Kotlin)

val instance = Person("홍길동", 25)
val clazz = Person::class // kotlin reflection에는 forName이 없다. (= Class.forName("A.B.C.Person").kotlin)

// 클래스에서 info 메소드를 얻고, 위에서 만든 instance 로 실행(invoke) 시킨다.
// 25세 홍길동()님
val infoMethodResult = clazz.functions.first { it.name == "info" }.call(instance) as String
println(infoMethodResult)

val field = (clazz.memberProperties.first { it.name == "nickName" } as KMutableProperty1<Person, String>).apply {
    this.isAccessible = true // accessible 을 설정하지 않고 set메소드를 호출하면 java.lang.IllegalAccessException 발생
    this.set(instance, "private 인데 외부에서 변경됨!!")
}

// nickName 필드는 아래와 같이 변경됐다.
// private 인데 외부에서 변경됨!!
val fieldValue = field.get(instance)
println(fieldValue)

// 다시 instance에서 info 메소드를 동적으로 호출하면 아래와같이 nickName 필드가 변경됐음을 확인할 수 있다.
// 25세 홍길동(private 인데 외부에서 변경됨!!)님
val infoMethodResult2 = clazz.functions.first { it.name == "info" }.call(instance) as String
println(infoMethodResult2)

private 인스턴스에 값 할당하기 (Java)

val instance = Person("홍길동", 25)
val nameClass = Class.forName("A.B.C.Person") // 클래스명으로도 찾을 수 있다. (= Person::class.java)

// 클래스에서 info 메소드를 얻고, 위에서 만든 instance 로 실행(invoke) 시킨다.
// 25세 홍길동()님
val infoMethodResult = nameClass.getMethod("info").invoke(instance) as String
println(infoMethodResult)

val field = nameClass.getDeclaredField("nickName").apply {
    this.setAccessible(true) // accessible 을 설정하지 않고 set메소드를 호출하면 java.lang.IllegalAccessException 발생
    this.set(instance, "private 인데 외부에서 변경됨!!")
}

// nickName 필드는 아래와 같이 변경됐다.
// private 인데 외부에서 변경됨!!
val fieldValue = field.get(instance) as String
println(fieldValue)

// 다시 instance에서 info 메소드를 동적으로 호출하면 아래와같이 nickName 필드가 변경됐음을 확인할 수 있다.
// 25세 홍길동(private 인데 외부에서 변경됨!!)님
val infoMethodResult2 = nameClass.getMethod("info").invoke(instance) as String
println(infoMethodResult2)

주의할 점!

위 예제에서 본대로, 리플렉션은 클래스/메소드/필드명을 string으로 직접 넣는다.
-> 컴파일 시점에 에러를 출력할 수 없다. (런타임 시점에 에러가 발생한다.)

일반적인 메소드 호출은 컴파일시점에 분석된 클래스를 사용하지만
리플렉션은 런타임시점에 동적으로 분석하므로 상대적으로 속도가 느리다.

클래스/메소드/필드를 직접 접근하여 사용하기 때문에, 객체지향의 추상화를 위반한다.

profile
A fast learner.

0개의 댓글