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 object나 object 내에 정의된 멤버는 기본적으로 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가 공존하는 프로젝트를 효율적으로 구축할 수 있습니다.