(Kotlin/코틀린) Java에서 Kotlin 호출 시 꼭 알아야 할 JVM 어노테이션 정리

개요


1. @JvmStatic

문제 상황

Kotlin companion object 안에 정의된 함수는 Java에서 보면 static이 아닙니다.

class MyClass {
    companion object {
        fun create(): MyClass = MyClass()
    }
}

Java에서 호출 시:

MyClass obj = MyClass.Companion.create(); // ❌ 어색함

@JvmStatic 적용

class MyClass {
    companion object {
        @JvmStatic
        fun create(): MyClass = MyClass()
    }
}

Java에서 호출 시:

MyClass obj = MyClass.create(); // ✅ 자연스러운 static 호출

동작 원리

object에서도 사용 가능

object Logger {
    @JvmStatic
    fun log(msg: String) = println(msg)
}
Logger.log("hello"); // ✅

2. @JvmField

문제 상황

Kotlin 프로퍼티는 Java에서 보면 getter/setter로 변환됩니다.

class User {
    val name: String = "홍길동"
}

Java에서 호출 시:

String name = user.getName(); // getter를 통해 접근

@JvmField 적용

class User {
    @JvmField
    val name: String = "홍길동"
}

Java에서 호출 시:

String name = user.name; // ✅ 필드로 직접 접근

동작 원리

companion object에서 상수 선언

class Config {
    companion object {
        @JvmField
        val BASE_URL = "https://api.example.com"
    }
}
String url = Config.BASE_URL; // ✅ static 필드처럼 접근

3. @JvmOverloads

문제 상황

Kotlin은 기본값(default parameter)을 지원하지만, Java는 지원하지 않습니다.

fun greet(name: String, greeting: String = "Hello") {
    println("$greeting, $name!")
}

Java에서 호출 시:

greet("홍길동", "Hello"); // 모든 파라미터를 명시해야 함
greet("홍길동");          // ❌ 컴파일 에러 - 오버로드가 없음

@JvmOverloads 적용

@JvmOverloads
fun greet(name: String, greeting: String = "Hello") {
    println("$greeting, $name!")
}

Java에서 호출 시:

greet("홍길동", "Hello"); // ✅
greet("홍길동");          // ✅ 오버로드 자동 생성

동작 원리

생성자에도 사용 가능

class Button @JvmOverloads constructor(
    context: Context,
    text: String = "",
    enabled: Boolean = true
)

4. @JvmName

문제 상황

Kotlin 확장 함수나 파일의 이름이 Java에서 보기 어색하거나 충돌이 생길 때 사용합니다.

// StringUtils.kt
fun String.isEmailValid(): Boolean { ... }

Java에서는 파일명 기반으로 StringUtilsKt.isEmailValid(str)로 호출됩니다.

@JvmName 적용 - 파일 이름 변경

@file:JvmName("StringUtils")

// StringUtils.kt
fun String.isEmailValid(): Boolean { ... }
StringUtils.isEmailValid(str); // ✅ 깔끔한 이름

@JvmName 적용 - 함수 이름 변경

@JvmName("isEmailValidJava")
fun String.isEmailValid(): Boolean { ... }
StringUtils.isEmailValidJava(str); // ✅

getter/setter 이름 변경

val isActive: Boolean
    @JvmName("isActive") get() = true

5. @JvmSuppressWildcards / @JvmWildcard

문제 상황

Kotlin 제네릭 타입이 Java에서 wildcard(? extends, ? super)로 변환될 때 의도와 다르게 동작하는 경우가 있습니다.

fun process(list: List<String>) { ... }
// Java에서는 List<? extends String>으로 변환될 수 있음

@JvmSuppressWildcards

fun process(list: List<@JvmSuppressWildcards String>) { ... }
// Java에서 List<String>으로 유지

@JvmWildcard

fun produce(): List<@JvmWildcard String>
// Java에서 List<? extends String>으로 강제 변환

6. @Throws

문제 상황

Kotlin은 checked exception이 없습니다. 하지만 Java에서는 checked exception을 try-catch로 처리해야 합니다.

fun readFile(path: String): String {
    return File(path).readText()
}

Java에서 호출 시 IOException을 잡으려 해도 컴파일러가 강제하지 않습니다.

@Throws 적용

@Throws(IOException::class)
fun readFile(path: String): String {
    return File(path).readText()
}

Java에서 호출 시:

try {
    String content = readFile("path/to/file"); // ✅ IOException 처리 강제
} catch (IOException e) {
    e.printStackTrace();
}

7. 어노테이션 요약

어노테이션 역할 주요 사용처
@JvmStatic companion object / object 멤버를 static으로 노출 팩토리 메서드, 유틸 함수
@JvmField 프로퍼티를 getter/setter 없이 public 필드로 노출 상수, 프레임워크 연동
@JvmOverloads 기본값 파라미터를 Java용 오버로드로 생성 생성자, 일반 함수
@JvmName 파일명 또는 함수명을 Java에서 보이는 이름으로 변경 확장 함수, 파일 이름 정리
@JvmSuppressWildcards 제네릭 타입의 wildcard 변환 억제 제네릭 함수
@JvmWildcard 제네릭 타입을 wildcard로 강제 변환 제네릭 함수
@Throws Kotlin 함수에 Java checked exception 선언 추가 Java 연동 코드

8. 실무 팁

companion object {
    const val MAX_COUNT = 100    // Java에서 static final로 접근 가능
    @JvmField val TAG = "MyTag"  // 런타임 값은 @JvmField 사용
}

참고



Related Posts