Post

Kotlin 의 Java 상호 운용성

Kotlin 의 Java 상호 운용성

Java와의 상호 운용성: Kotlin 프로젝트에서의 공존

Kotlin은 JVM 위에서 동작하며, 기존 Java 코드베이스와의 완벽한 상호 운용성을 목표로 설계되었습니다. 이는 대규모 Java 프로젝트에 Kotlin을 점진적으로 도입하거나, 기존 Java 라이브러리를 Kotlin 프로젝트에서 활용할 때 중요한 기반이 됩니다.


1. Kotlin에서 Java 코드 호출하기

Kotlin은 Java의 클래스, 인터페이스, 메서드, 필드를 마치 Kotlin의 요소처럼 자연스럽게 호출할 수 있습니다. 대부분의 Java 코드는 별다른 설정 없이 Kotlin 코드에서 직접 사용 가능합니다.

예시:

Java 코드 (JavaUtil.java)

1
2
3
4
5
6
7
8
9
10
11
package com.example.java;

public class JavaUtil {
    public static String greet(String name) {
        return "Hello, " + name + " from Java!";
    }

    public int add(int a, int b) {
        return a + b;
    }
}

Kotlin 코드 (KotlinApp.kt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.kotlin

import com.example.java.JavaUtil // Java 클래스 임포트

fun main() {
    // Java의 static 메서드 호출
    val message = JavaUtil.greet("Kotlin")
    println(message) // 출력: Hello, Kotlin from Java!

    // Java 클래스의 인스턴스 생성 및 메서드 호출
    val javaUtil = JavaUtil()
    val sum = javaUtil.add(10, 20)
    println("Sum from JavaUtil: $sum") // 출력: Sum from JavaUtil: 30
}

고려사항:

  • Nullability: Java는 Kotlin과 달리 null을 명시적으로 처리하는 타입 시스템이 없습니다. Kotlin이 Java 코드를 호출할 때, Java의 모든 참조 타입은 플랫폼 타입(Platform Types)으로 간주됩니다. 이는 Kotlin 컴파일러가 해당 타입의 nullability를 알 수 없으므로, 개발자가 직접 String? 또는 String 중 어느 것으로 처리할지 결정해야 함을 의미합니다. 만약 null이 될 수 없는 Java 값을 null이 가능한 Kotlin 타입으로 받으면 안전하지만, 그 반대의 경우(Java 값이 null인데 String으로 받으면) 런타임에 NullPointerException이 발생할 수 있습니다.
  • Checked Exceptions: Kotlin은 checked exception 개념이 없지만, Java 메서드가 checked exception을 던질 수 있음을 알고 있어야 합니다. Kotlin 코드에서 이러한 Java 메서드를 호출할 때는 try-catch 블록으로 예외를 처리하거나, throws 어노테이션 (@Throws(IOException::class))을 사용하여 Kotlin 함수가 Java 호출자에게 예외를 던진다는 것을 명시할 수 있습니다.

2. Java에서 Kotlin 코드 호출하기

Kotlin 코드를 작성할 때, Java 호출자가 어떻게 이 코드를 사용할지 염두에 두면 좋습니다. Kotlin 컴파일러는 Java 호출자를 위해 몇 가지 편의 기능을 제공합니다.

2.1. Top-Level Functions & Properties

Kotlin 파일의 최상위에 직접 정의된 함수나 프로퍼티는 해당 Kotlin 파일의 이름을 딴 클래스(FileNameKt.java)의 정적 멤버로 컴파일됩니다.

Kotlin 코드 (Utils.kt)

1
2
3
4
5
6
7
package com.example.kotlin

fun calculateSum(a: Int, b: Int): Int {
    return a + b
}

const val APP_VERSION = "1.0.0"

Java 코드 (JavaCaller.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.java;

import com.example.kotlin.UtilsKt; // 자동으로 생성된 클래스 임포트

public class JavaCaller {
    public static void main(String[] args) {
        // Kotlin의 최상위 함수 호출
        int sum = UtilsKt.calculateSum(5, 7);
        System.out.println("Sum from Kotlin: " + sum); // 출력: Sum from Kotlin: 12

        // Kotlin의 최상위 상수 호출
        String version = UtilsKt.APP_VERSION;
        System.out.println("App Version from Kotlin: " + version); // 출력: App Version from Kotlin: 1.0.0
    }
}

` ` 팁: @file:JvmName("CustomUtils") 어노테이션을 사용하여 생성될 클래스 이름을 변경할 수 있습니다.

2.2. @JvmStatic

Kotlin의 companion objectobject 내에 정의된 멤버는 기본적으로 Java에서 ClassName.Companion.method() 또는 ObjectName.INSTANCE.method() 형태로 접근해야 합니다. @JvmStatic 어노테이션을 사용하면 이 멤버들을 Java에서 정적(static) 멤버처럼 ClassName.method() 또는 ObjectName.method() 형태로 직접 호출할 수 있습니다.

Kotlin 코드 (Logger.kt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.kotlin

class Logger {
    companion object {
        @JvmStatic
        fun logMessage(message: String) {
            println("[Logger] $message")
        }
    }
}

object AppConfig {
    @JvmStatic
    val MAX_THREADS = 10
}

Java 코드 (JavaCaller.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.java;

import com.example.kotlin.Logger;
import com.example.kotlin.AppConfig;

public class JavaCaller {
    public static void main(String[] args) {
        // @JvmStatic 덕분에 Logger.Companion 없이 직접 호출
        Logger.logMessage("Application started."); // 출력: [Logger] Application started.

        // @JvmStatic 덕분에 AppConfig.INSTANCE 없이 직접 접근
        int maxThreads = AppConfig.MAX_THREADS;
        System.out.println("Max threads: " + maxThreads); // 출력: Max threads: 10
    }
}

` `

2.3. @JvmField

Kotlin의 프로퍼티는 기본적으로 Java에서 getter/setter 메서드로 변환됩니다. @JvmField 어노테이션을 사용하면 해당 프로퍼티를 Java에서 필드처럼 직접 접근할 수 있습니다. 이는 특히 public final 필드에 유용합니다.

Kotlin 코드 (UserSettings.kt)

1
2
3
4
5
6
package com.example.kotlin

class UserSettings(val userId: String) {
    @JvmField
    val defaultLocale: String = "en_US"
}

Java 코드 (JavaCaller.java)

1
2
3
4
5
6
7
8
9
10
11
package com.example.java;

import com.example.kotlin.UserSettings;

public class JavaCaller {
    public static void main(String[] args) {
        UserSettings settings = new UserSettings("user123");
        // @JvmField 덕분에 필드처럼 직접 접근
        System.out.println("Default locale: " + settings.defaultLocale); // 출력: Default locale: en_US
    }
}

` `

2.4. @JvmOverloads

Kotlin 함수에서 기본 파라미터(default parameters)를 사용하는 경우, Java에서는 해당 파라미터가 있는 하나의 메서드만 보입니다. @JvmOverloads를 사용하면 Kotlin 컴파일러가 기본 파라미터 조합에 따라 오버로드된 메서드들을 자동으로 생성해 줍니다.

Kotlin 코드 (GreetingService.kt)

1
2
3
4
5
6
7
8
package com.example.kotlin

class GreetingService {
    @JvmOverloads
    fun greet(name: String, prefix: String = "Hello", suffix: String = "!"): String {
        return "$prefix, $name$suffix"
    }
}

Java 코드 (JavaCaller.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.java;

import com.example.kotlin.GreetingService;

public class JavaCaller {
    public static void main(String[] args) {
        GreetingService service = new GreetingService();

        // Kotlin의 기본 파라미터 덕분에 여러 오버로드된 메서드가 Java에 노출됨
        System.out.println(service.greet("Kotlin"));            // 출력: Hello, Kotlin!
        System.out.println(service.greet("Java", "Hi"));       // 출력: Hi, Java!
        System.out.println(service.greet("World", "Greetings", "!!!")); // 출력: Greetings, World!!!
    }
}

` `

2.5. SAM (Single Abstract Method) 변환

Java 인터페이스 중 단일 추상 메서드만 가진(SAM 인터페이스) 경우, Kotlin에서는 람다 표현식으로 해당 인터페이스의 인스턴스를 간결하게 생성할 수 있습니다.

Java 코드 (MyClickListener.java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.java;

public interface MyClickListener {
    void onClick(String message);
}

public class Button {
    private MyClickListener listener;

    public void setOnClickListener(MyClickListener listener) {
        this.listener = listener;
    }

    public void click() {
        if (listener != null) {
            listener.onClick("Button clicked!");
        }
    }
}

Kotlin 코드 (KotlinApp.kt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.kotlin

import com.example.java.Button

fun main() {
    val button = Button()

    // Java의 SAM 인터페이스를 람다로 구현
    button.setOnClickListener { message ->
        println("Received from button: $message") // 출력: Received from button: Button clicked!
    }

    button.click()
}

` `


결론

Kotlin과 Java의 상호 운용성은 Kotlin의 주요 강점 중 하나입니다. 기존 Java 코드베이스를 활용하면서 점진적으로 Kotlin으로 전환하거나, Kotlin 프로젝트에서 풍부한 Java 라이브러리 생태계를 활용하는 것이 가능합니다. Kotlin 컴파일러가 제공하는 @JvmStatic, @JvmField, @JvmOverloads 등의 어노테이션을 적절히 사용하면, Java 호출자에게 더 자연스럽고 익숙한 API를 제공할 수 있습니다. 이러한 기능들을 이해하고 활용함으로써, Kotlin과 Java가 공존하는 프로젝트를 효율적으로 구축할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.