Kotlin Metadata JVM 라이브러리
Kotlin Metadata JVM 라이브러리
kotlin-metadata-jvm 라이브러리는 JVM용으로 컴파일된 Kotlin 클래스의 메타데이터를 읽고, 수정하고, 생성하는 도구를 제공해요. .class 파일 안의 @Metadata 애노테이션에 저장되는 이 메타데이터는 kotlin-reflect 같은 라이브러리와 도구가 런타임에 프로퍼티·함수·클래스 같은 Kotlin 전용 구조를 검사하는 데 사용해요.
kotlin-reflect 라이브러리는 런타임에 Kotlin 전용 클래스 세부 정보를 가져오기 위해 메타데이터에 의존해요. 메타데이터와 실제 .class 파일 사이에 불일치가 있으면 reflection을 사용할 때 잘못된 동작이 발생할 수 있어요.
Kotlin Metadata JVM 라이브러리로 가시성(visibility)이나 modality 같은 다양한 선언 속성을 검사하거나, 메타데이터를 생성해서 .class 파일에 임베딩할 수도 있어요.
본문
프로젝트에 라이브러리 추가하기
Kotlin Metadata JVM 라이브러리를 프로젝트에 포함하려면 빌드 도구에 따라 해당 의존성 구성을 추가해요.
Kotlin Metadata JVM 라이브러리는 Kotlin 컴파일러 및 표준 라이브러리와 같은 버전 규칙을 따릅니다. 사용하는 버전이 프로젝트의 Kotlin 버전과 일치하는지 확인해요.
Gradle
build.gradle(.kts) 파일에 다음 의존성을 추가해요:
// build.gradle.kts
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.20")
}
// build.gradle
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-metadata-jvm:2.4.20'
}
Maven
pom.xml 파일에 다음 의존성을 추가해요:
<project>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.4.20</version>
</dependency>
</dependencies>
...
</project>
메타데이터 읽고 파싱하기
kotlin-metadata-jvm 라이브러리는 컴파일된 Kotlin .class 파일에서 클래스 이름, 가시성, 시그니처 같은 구조화된 정보를 추출해요. 컴파일된 Kotlin 선언을 분석해야 하는 프로젝트에서 사용할 수 있어요. 예를 들어 Binary Compatibility Validator(BCV)는 kotlin-metadata-jvm에 의존해서 공개 API 선언을 출력해요.
reflection으로 컴파일된 클래스에서 @Metadata 애노테이션을 가져오는 것으로 Kotlin 클래스 메타데이터 탐색을 시작할 수 있어요:
fun main() {
// Specifies the fully qualified name of the class
val clazz = Class.forName("org.example.SampleClass")
// Retrieves the @Metadata annotation
val metadata = clazz.getAnnotation(Metadata::class.java)
// Checks if the metadata is present
if (metadata != null) {
println("This is a Kotlin class with metadata.")
} else {
println("This is not a Kotlin class.")
}
}
@Metadata 애노테이션을 가져온 뒤에는 KotlinClassMetadata API의 readLenient() 또는 readStrict() 함수를 사용해서 파싱해요. 이 함수들은 서로 다른 호환성 요구를 다루면서 클래스나 파일에 대한 자세한 정보를 추출해요:
- readLenient(): 더 새로운 Kotlin 컴파일러 버전이 생성한 메타데이터를 포함해 메타데이터를 읽는 데 사용해요. 이 함수는 메타데이터 수정이나 쓰기를 지원하지 않아요.
- readStrict(): 메타데이터를 수정하고 써야 할 때 사용해요.
readStrict()함수는 프로젝트가 완전히 지원하는 Kotlin 컴파일러 버전이 생성한 메타데이터로만 동작해요.
readStrict() 함수는 JvmMetadataVersion.LATEST_STABLE_SUPPORTED보다 한 버전 뒤까지의 메타데이터 형식을 지원해요. 이 값은 프로젝트에 사용된 최신 Kotlin 버전에 해당해요. 예를 들어 프로젝트가 kotlin-metadata-jvm:2.1.0에 의존한다면 readStrict()는 Kotlin 2.2.x까지의 메타데이터를 처리할 수 있고, 그 너머는 알 수 없는 형식을 잘못 다루는 것을 막으려고 오류를 던져요.
자세한 내용은 Kotlin Metadata GitHub 리포지토리를 참조해요.
메타데이터를 파싱할 때 KotlinClassMetadata 인스턴스는 클래스 또는 파일 레벨 선언에 대한 구조화된 정보를 제공해요. 클래스의 경우 kmClass 프로퍼티를 사용해 클래스 이름, 함수, 프로퍼티, 가시성 같은 속성 같은 상세한 클래스 레벨 메타데이터를 분석해요. 파일 레벨 선언의 경우 메타데이터는 kmPackage 프로퍼티로 표현되며, 여기에는 Kotlin 컴파일러가 생성한 파일 퍼사드(file facade)의 최상위 함수와 프로퍼티가 포함돼요.
다음 코드 예시는 readLenient()으로 메타데이터를 파싱하고, kmClass로 클래스 레벨 세부 정보를 분석하고, kmPackage로 파일 레벨 선언을 가져오는 방법을 보여줘요:
// Imports the necessary libraries
import kotlin.metadata.jvm.*
import kotlin.metadata.*
fun main() {
// Specifies the fully qualified class name
val className = "org.example.SampleClass"
try {
// Retrieves the class object for the specified name
val clazz = Class.forName(className)
// Retrieves the @Metadata annotation
val metadataAnnotation = clazz.getAnnotation(Metadata::class.java)
if (metadataAnnotation != null) {
println("Kotlin Metadata found for class: $className")
// Parses metadata using the readLenient() function
val metadata = KotlinClassMetadata.readLenient(metadataAnnotation)
when (metadata) {
is KotlinClassMetadata.Class -> {
val kmClass = metadata.kmClass
println("Class name: ${kmClass.name}")
// Iterates over functions and checks visibility
kmClass.functions.forEach { function ->
val visibility = function.visibility
println("Function: ${function.name}, Visibility: $visibility")
}
}
is KotlinClassMetadata.FileFacade -> {
val kmPackage = metadata.kmPackage
// Iterates over functions and checks visibility
kmPackage.functions.forEach { function ->
val visibility = function.visibility
println("Function: ${function.name}, Visibility: $visibility")
}
}
else -> {
println("Unsupported metadata type: $metadata")
}
}
} else {
println("No Kotlin Metadata found for class: $className")
}
} catch (e: ClassNotFoundException) {
println("Class not found: $className")
} catch (e: Exception) {
println("Error processing metadata: ${e.message}")
e.printStackTrace()
}
}
메타데이터에서 애노테이션 쓰고 읽기
Kotlin은 애노테이션을 바이트코드와 Kotlin 메타데이터 양쪽에 저장해요. kotlin-metadata-jvm 라이브러리로 애노테이션을 읽거나 쓴다면, 그것들의 메타데이터 표현을 다루게 돼요.
Kotlin은 Kotlin 2.4.0부터 Kotlin 메타데이터에 애노테이션을 저장해요. 그 이전 버전으로 컴파일된 클래스 파일을 검사한다면, 애노테이션이 메타데이터에 없을 수 있어요.
메타데이터에서 애노테이션을 바꿀 때는 바이트코드에 저장된 애노테이션과 일관되게 유지해야 해요. 동기화되지 않으면 reflection이나 바이트코드 분석에 의존하는 도구가 Kotlin 메타데이터를 읽는 도구와 다른 결과를 보고할 수 있어요.
kotlin-metadata-jvm 라이브러리는 애노테이션에 접근하기 위한 다음 API들을 제공해요:
- KmClass.annotations
- KmFunction.annotations
- KmProperty.annotations
- KmConstructor.annotations
- KmPropertyAccessorAttributes.annotations
- KmValueParameter.annotations
- KmFunction.extensionReceiverAnnotations
- KmProperty.extensionReceiverAnnotations
- KmProperty.backingFieldAnnotations
- KmProperty.delegateFieldAnnotations
- KmEnumEntry.annotations
Kotlin 메타데이터에서 애노테이션을 읽는 예시는 다음과 같아요:
import kotlin.metadata.ExperimentalAnnotationsInMetadata
import kotlin.metadata.jvm.KotlinClassMetadata
annotation class Label(val value: String)
@Label("Message class")
class Message
fun main() {
val metadata = Message::class.java.getAnnotation(Metadata::class.java)
val kmClass = (KotlinClassMetadata.readStrict(metadata) as KotlinClassMetadata.Class).kmClass
println(kmClass.annotations)
// [@Label(value = StringValue("Message class"))]
}
바이트코드에서 메타데이터 추출하기
reflection으로 메타데이터를 가져올 수 있지만, 또 다른 방법은 ASM 같은 바이트코드 조작 프레임워크를 사용해 바이트코드에서 추출하는 거예요.
이렇게 하면 돼요:
- ASM 라이브러리의
ClassReader클래스로.class파일의 바이트코드를 읽어요. 이 클래스는 컴파일된 파일을 처리하고 클래스 구조를 나타내는ClassNode객체를 채워요. ClassNode객체에서@Metadata를 추출해요. 아래 예시는 이를 위해findAnnotation()사용자 정의 확장 함수를 사용해요.KotlinClassMetadata.readLenient()함수로 추출한 메타데이터를 파싱해요.kmClass와kmPackage프로퍼티로 파싱된 메타데이터를 검사해요.
예시는 다음과 같아요:
// Imports the necessary libraries
import kotlin.metadata.jvm.*
import kotlin.metadata.*
import org.objectweb.asm.*
import org.objectweb.asm.tree.*
import java.io.File
// Checks if an annotation refers to a specific name
fun AnnotationNode.refersToName(name: String) =
desc.startsWith('L') && desc.endsWith(';') && desc.regionMatches(1, name, 0, name.length)
// Retrieves annotation values by key
private fun List<Any>.annotationValue(key: String): Any? {
for (index in (0 until size / 2)) {
if (this[index * 2] == key) {
return this[index * 2 + 1]
}
}
return null
}
// Defines a custom extension function to locate an annotation by its name in a ClassNode
fun ClassNode.findAnnotation(annotationName: String, includeInvisible: Boolean = false): AnnotationNode? {
val visible = visibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
if (!includeInvisible) return visible
return visible ?: invisibleAnnotations?.firstOrNull { it.refersToName(annotationName) }
}
// Operator to simplify retrieving annotation values
operator fun AnnotationNode.get(key: String): Any? = values.annotationValue(key)
// Extracts Kotlin metadata from a class node
fun ClassNode.readMetadataLenient(): KotlinClassMetadata? {
val metadataAnnotation = findAnnotation("kotlin/Metadata", false) ?: return null
@Suppress("UNCHECKED_CAST")
val metadata = Metadata(
kind = metadataAnnotation["k"] as Int?,
metadataVersion = (metadataAnnotation["mv"] as List<Int>?)?.toIntArray(),
data1 = (metadataAnnotation["d1"] as List<String>?)?.toTypedArray(),
data2 = (metadataAnnotation["d2"] as List<String>?)?.toTypedArray(),
extraString = metadataAnnotation["xs"] as String?,
packageName = metadataAnnotation["pn"] as String?,
extraInt = metadataAnnotation["xi"] as Int?
)
return KotlinClassMetadata.readLenient(metadata)
}
// Converts a file to a ClassNode for bytecode inspection
fun File.toClassNode(): ClassNode {
val node = ClassNode()
this.inputStream().use { ClassReader(it).accept(node, ClassReader.SKIP_CODE) }
return node
}
fun main() {
val classFilePath = "build/classes/kotlin/main/org/example/SampleClass.class"
val classFile = File(classFilePath)
// Reads the bytecode and processes it into a ClassNode object
val classNode = classFile.toClassNode()
// Locates the @Metadata annotation and reads it leniently
val metadata = classNode.readMetadataLenient()
if (metadata != null && metadata is KotlinClassMetadata.Class) {
// Inspects the parsed metadata
val kmClass = metadata.kmClass
// Prints class details
println("Class name: ${kmClass.name}")
println("Functions:")
kmClass.functions.forEach { function ->
println("- ${function.name}, Visibility: ${function.visibility}")
}
}
}
메타데이터 수정하기
ProGuard 같은 도구로 바이트코드를 축소하고 최적화할 때는 일부 선언이 .class 파일에서 제거될 수 있어요. ProGuard는 수정된 바이트코드와 일관되게 유지하도록 메타데이터를 자동으로 업데이트해요.
하지만 Kotlin 바이트코드를 비슷한 방식으로 수정하는 사용자 정의 도구를 개발한다면, 메타데이터도 그에 맞게 조정해야 해요. kotlin-metadata-jvm 라이브러리로 선언을 업데이트하고, 속성을 조정하고, 특정 요소를 제거할 수 있어요.
예를 들어 Java 클래스 파일에서 private 메서드를 삭제하는 JVM 도구를 사용한다면, 일관성을 유지하려면 Kotlin 메타데이터에서도 private 함수를 삭제해야 해요:
readStrict()함수로 메타데이터를 파싱해서@Metadata애노테이션을 구조화된KotlinClassMetadata객체로 로드해요.kmClass나 다른 메타데이터 구조 안에서 함수를 필터링하거나 속성을 바꾸는 등 메타데이터를 조정해서 수정을 적용해요.write()함수로 수정된 메타데이터를 새@Metadata애노테이션으로 인코딩해요.
다음은 클래스 메타데이터에서 private 함수가 제거되는 예시예요:
// Imports the necessary libraries
import kotlin.metadata.jvm.*
import kotlin.metadata.*
fun main() {
// Specifies the fully qualified class name
val className = "org.example.SampleClass"
try {
// Retrieves the class object for the specified name
val clazz = Class.forName(className)
// Retrieves the @Metadata annotation
val metadataAnnotation = clazz.getAnnotation(Metadata::class.java)
if (metadataAnnotation != null) {
println("Kotlin Metadata found for class: $className")
// Parses metadata using the readStrict() function
val metadata = KotlinClassMetadata.readStrict(metadataAnnotation)
if (metadata is KotlinClassMetadata.Class) {
val kmClass = metadata.kmClass
// Removes private functions from the class metadata
kmClass.functions.removeIf { it.visibility == Visibility.PRIVATE }
println("Removed private functions. Remaining functions: ${kmClass.functions.map { it.name }}")
// Serializes the modified metadata back
val newMetadata = metadata.write()
// After modifying the metadata, you need to write it into the class file
// To do so, you can use a bytecode manipulation framework such as ASM
println("Modified metadata: ${newMetadata}")
} else {
println("The metadata is not a class.")
}
} else {
println("No Kotlin Metadata found for class: $className")
}
} catch (e: ClassNotFoundException) {
println("Class not found: $className")
} catch (e: Exception) {
println("Error processing metadata: ${e.message}")
e.printStackTrace()
}
}
readStrict()와 write()를 따로 호출하는 대신 transform() 함수를 사용할 수 있어요. 이 함수는 메타데이터를 파싱하고 람다를 통해 변환을 적용한 다음 수정된 메타데이터를 자동으로 써요.
메타데이터 처음부터 생성하기
Kotlin Metadata JVM 라이브러리로 Kotlin 클래스 파일용 메타데이터를 처음부터 만들려면:
- 생성할 메타데이터 유형에 따라
KmClass,KmPackage, 또는KmLambda인스턴스를 만들어요. - 클래스 이름, 가시성, 생성자, 함수 시그니처 같은 속성을 인스턴스에 추가해요.
프로퍼티를 설정하면서
apply()스코프 함수를 사용하면 보일러플레이트 코드를 줄일 수 있어요. - 인스턴스로
KotlinClassMetadata객체를 만들어요. 이 객체는@Metadata애노테이션을 생성할 수 있어요. JvmMetadataVersion.LATEST_STABLE_SUPPORTED같은 메타데이터 버전을 지정하고 플래그를 설정해요(0은 플래그 없음, 필요하면 기존 파일의 플래그를 복사).- ASM의
ClassWriter클래스로kind,data1,data2같은 메타데이터 필드를.class파일에 임베딩해요.
다음 예시는 간단한 Kotlin 클래스용 메타데이터를 만드는 방법을 보여줘요:
// Imports the necessary libraries
import kotlin.metadata.*
import kotlin.metadata.jvm.*
import org.objectweb.asm.*
fun main() {
// Creates a KmClass instance
val klass = KmClass().apply {
name = "Hello"
visibility = Visibility.PUBLIC
constructors += KmConstructor().apply {
visibility = Visibility.PUBLIC
signature = JvmMethodSignature("<init>", "()V")
}
functions += KmFunction("hello").apply {
visibility = Visibility.PUBLIC
returnType = KmType().apply {
classifier = KmClassifier.Class("kotlin/String")
}
signature = JvmMethodSignature("hello", "()Ljava/lang/String;")
}
}
// Serializes a KotlinClassMetadata.Class instance, including the version and flags, into a @kotlin.Metadata annotation
val annotationData = KotlinClassMetadata.Class(
klass, JvmMetadataVersion.LATEST_STABLE_SUPPORTED, 0
).write()
// Generates a .class file with ASM
val classBytes = ClassWriter(0).apply {
visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Hello", null, "java/lang/Object", null)
// Writes @kotlin.Metadata instance to the .class file
visitAnnotation("Lkotlin/Metadata;", true).apply {
visit("mv", annotationData.metadataVersion)
visit("k", annotationData.kind)
visitArray("d1").apply {
annotationData.data1.forEach { visit(null, it) }
visitEnd()
}
visitArray("d2").apply {
annotationData.data2.forEach { visit(null, it) }
visitEnd()
}
visitEnd()
}
visitEnd()
}.toByteArray()
// Writes the generated .class file to disk
java.io.File("Hello.class").writeBytes(classBytes)
println("Metadata and .class file created successfully.")
}
더 자세한 예시는 Kotlin Metadata JVM GitHub 리포지토리를 참조해요.
더 알아보기
- Kotlin Metadata JVM 라이브러리의 API 참조를 확인해요.
- Kotlin Metadata JVM GitHub 리포지토리를 살펴보세요.
- 모듈 메타데이터와
.kotlin_module파일 작업에 대해 배워요.