본문 바로가기
Kotlin

왜 reflect.jar 파일이 별도로 존재하지?

by oncerun 2023. 7. 6.
반응형

 

리플렉션은 런타임에 프로그램의 구조를 파악할 수 있게 해주는 언어 및 라이브러리 features의 집합이다.

 

코틀린에서 함수와 속성은 일급 시민이다. 이를 검사하는 기능은 기능적 또는 반응형 스타일을 사용할 때 필수적이다.


일급 시민과 리플렉션은 직접적인 관계는 없습니다. 코틀린의 언어적 특성에 함수와 속성 등은 일급 시민이라는 것입니다.

 

다만 리플렉션을 통해 메타데이터(클래스, 함수, 프로퍼티 등)를 동적으로 액세스 하고 조작할 수 있다는 것입니다.

 

그리고 이러한 기능을 사용하는 경우 우리는 별도의 라이브러리인 kotlin-reflect.jar를 사용해야 한다는 것입니다.

 

이를 분리한 이유는 공식문서에서 설명한 것과 같이 다음과 같습니다. 

 

JVM 플랫폼에서 Kotlin 컴파일러 배포에는 리플렉션 기능을 사용하는 데 필요한 런타임 구성 요소가 별도의 아티팩트인 kotlin-reflect.jar로 포함되어 있습니다.

이는 리플렉션 기능을 사용하지 않는 애플리케이션에 필요한 런타임 라이브러리의 크기를 줄이기 위해서입니다.

 

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
}

 

이는 코틀린을 사용하여 런타임에 코틀린 코드를 리플렉션 하기 위해선 해당 라이브러리가 필요하다는 것으로 귀결됩니다.

 

저는 다음이 궁금해졌습니다. 

 

코프링을 사용하다 보면 jpa, spring, openAll와 같은 플러그인을 추가하는 경우가 있습니다. 

 

이 플러그인들의 용도는 각각 다음과 같습니다.

 

JPA

 -  jpa Spec에 따르면 기본 생성자는 반드시 필요하다. 이는 엔티티를 런타임에 생성할 때 이때 메타데이터가 필요하기 때문이며, 이를 위해선 기본 생성자를 사용하기 때문이다. 

만약 코틀린에서 생성자를 다음과 같이 정의한 경우 문제가 될 수 있다.

 

@Entity
class User constructor(
    var name:String,
    val age:Int?,
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
) {
	
    //기본 생성자가 필요하여 추가한다면?
    constructor() : this("", null)
	
}

 

 

코틀린에서는 생성자를 추가하기 위해 사전에 정의한 생성자를 오버로드하여 구성하도록 정의되어 있습니다.

결국 기본 생성자를 생성하기 위해선 정의된 생성자를 호출할 수밖에 없습니다.

 

https://kotlinlang.org/docs/no-arg-plugin.html

 

No-arg compiler plugin | Kotlin

 

kotlinlang.org

 

다음 공식문서에는 이렇게 생성된 기본 생성자를 Java와 코틀린에서 직접적으로 호출할 수는 없지만 reflection을 사용하여 호출할 수 있다고 합니다. 

 

이를 위해 아마 kotlin relfect.jar 파일을 사용하는데, 이를 클래스 패스에서 찾아 동작하도록 하는 것으로 예상됩니다.

 

테스트를 위해 해당 라이브러리를 제거하고 컴파일하겠습니다. 다음과 같은 build.gradle 파일을 만듭니다.

 

plugins {
    id 'org.springframework.boot' version '2.6.8'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.8.22'
    id 'org.jetbrains.kotlin.plugin.jpa' version '1.8.22'
}

...

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
//    implementation 'org.jetbrains.kotlin:kotlin-reflect'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

애플리케이션 빌드 시 최하단의 에러로그는 다음과 같은 오류가 발생합니다.

 

Caused by: java.lang.ClassNotFoundException: kotlin.reflect.full.KClasses
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
	at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
	... 63 common frames omitted

 

이를 통해 jpa 플러그인이 kotlin.reflect.jar 라이브러리에 의존한다고 생각할 수 있습니다.

 

 

spring, allOpen

 

Kotlin은 기본적으로 클래스와 속성(멤버)을  final로 취급합니다. 하지만 상속이 필요한  Spring AOP와 같은 프레임워크 및 라이브러리를 사용하는 데 불편함이 있습니다.

 

All-open compiler 플러그인은 이러한 프레임워크의 요구 사항에 맞게 Kotlin을 조정하고 특정 어노테이션으로 주석이 지정된 클래스와 해당 멤버를 명시적인 open 키워드 없이도 열 수 있도록 합니다.

 

spring은 all-open의 wrapper로 정확히 같은 방식으로 동작합니다. 

단지 기본적으로 @Component, @Async, @Transactional, @Cacheable, @SpringBootTest 등등..  해당 어노테이션이 달린 클래스는 자동으로 상속 가능하도록 open 키워드가 생성되는 것 같습니다.

물론 프로젝트에서 kotlin-allopen과 kotlin-spring을 동시에 사용할 수 있습니다.

https://kotlinlang.org/docs/all-open-plugin.html#gradle

 

All-open compiler plugin | Kotlin

 

kotlinlang.org

 

그럼 해당 플러그인도 kotlin-reflect.jar 파일이 필요한가에 대해 알아보았습니다.

 

깃헙에서는  찾을 수 없었지만 정확하지 않았습니다. 어떤 코드를 봐야 하는지 애매했고 확인된 코틀린 클래스에서는 직접적으로 reflect api를 사용하는 부분을 찾지 못했습니다.

https://github.com/JetBrains/kotlin/tree/master/plugins/allopen

 

GitHub - JetBrains/kotlin: The Kotlin Programming Language.

The Kotlin Programming Language. . Contribute to JetBrains/kotlin development by creating an account on GitHub.

github.com

 

 

그래서 그냥 테스트했습니다.

 

plugins {
    id 'org.springframework.boot' version '2.7.13'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'org.jetbrains.kotlin.jvm' version '1.6.21'
    id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'
}


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
    //implementation 'org.jetbrains.kotlin:kotlin-reflect'
    runtimeOnly 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

실행 및 api 테스트에도 문제없이 동작했습니다.

 

테스트는 @Transactional을 사용하여 서비스 클래스가 프락시 대상이 되도록 했으며, 이는 open 되지 않았다면 분명 오류가 발생했어야 합니다. 

 

생각엔 컴파일 단계에서 플러그인이 동작하여 리플렉션은 사용하지 않는 것 같습니다.

 

 

정리

 

왜 kotlin-reflect은 분리되어 있는가?

 

이는 리플렉션 기능을 사용하지 않는 애플리케이션에 필요한 런타임 라이브러리의 크기를 줄이기 위해서입니다.

 

 

Kopring에서 자주 사용하는 플러그인 중 kotlin-reflect에 의존하는 플러그인은 무엇인가?

(지속적인 추가 예정)

 

사용하는 플러그인

  • org.jetbrains.kotlin.plugin.jpa : 기본 생성자를 리플렉션을 통해 생성하기 위함.

사용하지 않는 플러그인

  • org.jetbrains.kotlin.plugin.spring (allopen) : 컴파일 시점에 상속가능한 클래스로 만들어 버림. 따라서 런타임에 리플렉션이 필요 없음.

 

 

 

나무 위키 (reflection, first-class_citizen) , 코틀린 공식 문서를 참조하였습니다.

반응형

'Kotlin' 카테고리의 다른 글

Scope functions  (0) 2023.07.15
Using JPA with Kotlin in a Spring Boot Application  (0) 2023.07.05
spring boot, gradle 설정에 kotlin 환경 설정하기.  (0) 2023.07.04
JUnit5 @JvmStatic  (0) 2023.07.04
Backing Property  (0) 2023.07.03

댓글