Theme:

실무에서 코틀린을 도입하면 기존 자바 코드와 함께 쓰는 경우가 대부분입니다. 이때 "자바에서 코틀린 코드를 호출했는데 Companion이 뜨는데요?"라거나 "기본 파라미터가 자바에서 안 먹혀요" 같은 문제를 겪게 됩니다. 면접에서도 상호운용 관련 질문이 꽤 나옵니다.

@JvmStatic — companion object를 자바 static으로

코틀린에는 static 키워드가 없습니다. 대신 companion object를 사용합니다.

KOTLIN
class ApiClient {
    companion object {
        fun create(): ApiClient = ApiClient()
    }
}

그런데 이걸 자바에서 호출하면 이렇게 됩니다.

JAVA
// 자바에서 호출
ApiClient client = ApiClient.Companion.create();  // Companion이 붙음

@JvmStatic을 붙이면 진짜 자바 static 메서드가 생성됩니다.

KOTLIN
class ApiClient {
    companion object {
        @JvmStatic
        fun create(): ApiClient = ApiClient()
    }
}
JAVA
// 자바에서 호출 — 깔끔해짐
ApiClient client = ApiClient.create();

object 선언에서도 동일하게 적용됩니다.

KOTLIN
object Logger {
    @JvmStatic
    fun log(message: String) {
        println(message)
    }
}
JAVA
// @JvmStatic 없으면: Logger.INSTANCE.log("hello")
// @JvmStatic 있으면: Logger.log("hello")
Logger.log("hello");

@JvmField — getter/setter 없이 필드 직접 노출

코틀린 프로퍼티는 컴파일 시 자동으로 getter/setter가 생성됩니다.

KOTLIN
class User(val name: String, var age: Int)

자바에서는 이렇게 접근해야 합니다.

JAVA
User user = new User("김철수", 25);
String name = user.getName();  // getter를 통해 접근
user.setAge(26);               // setter를 통해 접근

@JvmField를 붙이면 getter/setter 없이 필드로 직접 접근할 수 있습니다.

KOTLIN
class Config {
    @JvmField
    val maxRetry = 3

    companion object {
        @JvmField
        val DEFAULT_TIMEOUT = 5000
    }
}
JAVA
// @JvmField 덕분에 직접 접근 가능
int retry = config.maxRetry;
int timeout = Config.DEFAULT_TIMEOUT;

const val과의 차이

KOTLIN
companion object {
    const val MAX_SIZE = 100       // 컴파일 타임 상수, 자바에서 static final
    @JvmField val DEFAULT = "abc"  // 런타임 값, 자바에서 public 필드
}
  • const val: 컴파일 타임 상수. 기본 타입과 String만 가능
  • @JvmField: 런타임 값도 가능. 모든 타입에 사용 가능

@JvmOverloads — 기본 파라미터를 자바에서도

코틀린의 기본 파라미터(default parameter)는 자바에서 인식되지 않습니다.

KOTLIN
fun greet(name: String, greeting: String = "안녕", suffix: String = "!") {
    println("$greeting, $name$suffix")
}
JAVA
// 자바에서는 모든 파라미터를 전달해야 함
greet("김철수", "안녕", "!");     // OK
greet("김철수");                  // 컴파일 에러!

@JvmOverloads를 붙이면 오버로드 메서드가 자동 생성됩니다.

KOTLIN
@JvmOverloads
fun greet(name: String, greeting: String = "안녕", suffix: String = "!") {
    println("$greeting, $name$suffix")
}
JAVA
// 자바에서 세 가지 오버로드 사용 가능
greet("김철수");                  // greet("김철수", "안녕", "!")
greet("김철수", "하이");          // greet("김철수", "하이", "!")
greet("김철수", "하이", "~");     // greet("김철수", "하이", "~")

생성자에서의 @JvmOverloads

Android의 커스텀 View에서 특히 중요합니다.

KOTLIN
class CustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr)

이렇게 하면 자바에서 필요한 세 가지 생성자가 모두 생성됩니다.

주의할 점이 있습니다. @JvmOverloads는 파라미터를 왼쪽부터 순서대로 추가하며 오버로드를 생성합니다. 중간 파라미터만 생략하는 오버로드는 만들지 않습니다.

KOTLIN
// f(a), f(a, b), f(a, b, c)는 생성되지만
// f(a, c)는 생성되지 않음
@JvmOverloads
fun f(a: Int, b: Int = 0, c: Int = 0) {}

@JvmName — 이름 충돌 해결

코틀린에서는 유효하지만 자바에서 충돌이 발생하는 경우가 있습니다.

KOTLIN
// 코틀린에서는 OK: 반환 타입이 다르면 다른 함수
fun List<String>.filterValid(): List<String> = this
fun List<Int>.filterValid(): List<Int> = this

// 하지만 JVM에서는 타입 소거 때문에 시그니처가 같음!
// → 컴파일 에러

@JvmName으로 JVM에서 사용할 이름을 다르게 지정할 수 있습니다.

KOTLIN
@JvmName("filterValidStrings")
fun List<String>.filterValid(): List<String> = this

@JvmName("filterValidInts")
fun List<Int>.filterValid(): List<Int> = this

파일 수준에서 클래스 이름을 지정할 때도 사용합니다.

KOTLIN
// StringUtils.kt 파일
@file:JvmName("StringUtils")

package com.example

fun String.toSlug(): String = this.lowercase().replace(" ", "-")
JAVA
// 자바에서 호출
// @file:JvmName 없으면: StringUtilsKt.toSlug("Hello World")
// @file:JvmName 있으면: StringUtils.toSlug("Hello World")
String slug = StringUtils.toSlug("Hello World");

SAM 변환 — 람다로 인터페이스 구현

SAM(Single Abstract Method) 변환은 추상 메서드가 1개인 인터페이스를 람다로 간결하게 표현하는 기능입니다.

자바 인터페이스에 대한 SAM 변환

자바의 함수형 인터페이스는 코틀린에서 자동으로 SAM 변환됩니다.

JAVA
// 자바 인터페이스
public interface OnClickListener {
    void onClick(View view);
}
KOTLIN
// 코틀린에서 SAM 변환 — 자바 인터페이스는 자동 적용
button.setOnClickListener { view ->
    println("클릭: $view")
}

// SAM 변환 없이 풀어쓰면 이렇게 됨
button.setOnClickListener(object : OnClickListener {
    override fun onClick(view: View) {
        println("클릭: $view")
    }
})

코틀린 인터페이스의 SAM 변환 — fun interface

코틀린 인터페이스에는 SAM 변환이 자동으로 적용되지 않습니다. fun 키워드를 붙여야 합니다.

KOTLIN
// fun 없으면 SAM 변환 불가
interface Transformer {
    fun transform(input: String): String
}

// fun 붙이면 SAM 변환 가능
fun interface Transformer {
    fun transform(input: String): String
}

// 사용
val upper: Transformer = Transformer { it.uppercase() }
println(upper.transform("hello"))  // HELLO

면접에서 "코틀린 인터페이스는 왜 기본적으로 SAM 변환이 안 되나요?"라는 질문이 나올 수 있습니다. 코틀린은 함수 타입 (String) -> String이 있기 때문에 인터페이스 대신 함수 타입을 쓰는 것을 권장합니다. fun interface는 자바와의 호환성이나 타입에 이름을 붙이고 싶을 때 사용합니다.

플랫폼 타입 — null 안전성의 사각지대

코틀린의 가장 큰 장점 중 하나가 null 안전성인데, 자바 코드를 호출할 때는 이 보장이 깨집니다.

JAVA
// 자바 코드 — null 어노테이션 없음
public class JavaUtils {
    public static String process(String input) {
        return input.length() > 0 ? input : null;  // null 반환 가능!
    }
}
KOTLIN
// 코틀린에서 호출
val result = JavaUtils.process("hello")
// result의 타입: String! (플랫폼 타입)

// 위험: null일 수 있는데 non-null로 취급
val length = result.length  // NPE 발생 가능!

// 안전한 방법
val safeResult: String? = JavaUtils.process("hello")
val length = safeResult?.length

플랫폼 타입(String!)은 코틀린 코드에서 직접 선언할 수 없고, 자바 코드를 호출할 때만 나타납니다. null 여부를 컴파일러가 판단할 수 없으므로, 개발자가 명시적으로 String?이나 String으로 처리해야 합니다.

null 어노테이션으로 해결

자바 코드에 @Nullable, @NotNull 어노테이션이 있으면 코틀린 컴파일러가 이를 인식합니다.

JAVA
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;

public class JavaUtils {
    @NotNull
    public static String getName() { return "name"; }

    @Nullable
    public static String getEmail() { return null; }
}
KOTLIN
val name: String = JavaUtils.getName()    // OK — @NotNull이므로 String
val email: String? = JavaUtils.getEmail() // OK — @Nullable이므로 String?

자바에서 코틀린 호출 시 주의사항 모음

1. 코틀린 키워드가 자바 식별자인 경우

KOTLIN
// 코틀린에서 자바의 is, in 같은 키워드를 쓸 때
javaObject.`is`()    // 백틱으로 감싸기
javaList.`in`(value)

2. 확장 함수는 자바에서 static 메서드

KOTLIN
// StringExt.kt
fun String.addExclamation(): String = "$this!"
JAVA
// 자바에서 호출 — 첫 번째 인자로 receiver 전달
String result = StringExtKt.addExclamation("hello");

3. 코틀린의 Nothing 타입

KOTLIN
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

자바에서는 Void를 반환하는 것으로 보입니다. 하지만 실제로는 항상 예외를 던지므로 반환값을 사용할 일이 없습니다.

4. 코틀린의 internal 가시성

코틀린의 internal은 같은 모듈에서만 접근 가능하지만, JVM에서는 public으로 컴파일됩니다. 대신 이름이 맹글링(mangling)되어 자바에서 직접 호출하기 어렵게 만듭니다.

KOTLIN
internal fun secretFunction() {}
// 컴파일 후: public void secretFunction$module_name() {}

상호운용 체크리스트

실무에서 코틀린과 자바를 함께 쓸 때 체크할 항목들입니다.

KOTLIN
// 1. companion object의 상수/메서드 → @JvmStatic, @JvmField
companion object {
    @JvmStatic fun create() = MyClass()
    @JvmField val DEFAULT = MyClass()
    const val MAX = 100  // const val은 자동으로 static final
}

// 2. 기본 파라미터가 있는 함수 → @JvmOverloads
@JvmOverloads
fun connect(host: String, port: Int = 8080, timeout: Int = 3000) {}

// 3. 파일 수준 함수의 클래스명 → @file:JvmName
@file:JvmName("DateUtils")
fun parseDate(input: String): LocalDate = TODO()

// 4. 제네릭 타입 소거로 인한 충돌 → @JvmName
@JvmName("sortStrings")
fun List<String>.customSort(): List<String> = sorted()

// 5. 코틀린 인터페이스의 SAM 변환 → fun interface
fun interface Validator {
    fun validate(input: String): Boolean
}

정리

코틀린-자바 상호운용의 핵심 포인트를 면접용으로 요약하면 이렇습니다.

  • @JvmStatic: companion object 멤버를 자바에서 static으로 접근 가능하게 함
  • @JvmField: getter/setter 없이 필드로 직접 노출
  • @JvmOverloads: 기본 파라미터의 오버로드 메서드 자동 생성
  • @JvmName: JVM 이름 충돌 해결, 파일 클래스명 지정
  • SAM 변환: 자바 인터페이스는 자동, 코틀린은 fun interface 필요
  • 플랫폼 타입: 자바 코드의 null 안전성 사각지대. @Nullable/@NotNull 어노테이션 활용
댓글 로딩 중...