Kotlin-자바 상호운용성 가이드

이 문서는 다른 언어에서 사용될 때 코드가 직관적으로 느껴지도록 자바와 Kotlin에서 공개 API를 작성하는 규칙 세트입니다.

최종 업데이트: 2018년 5월 18일

자바(Kotlin 사용의 경우)

하드 키워드 없음

메서드나 필드 이름으로 Kotlin의 하드 키워드를 사용하지 마세요. Kotlin에서 호출할 때 백틱을 사용하여 이스케이프해야 합니다. 소프트 키워드, 한정자 키워드, 특수 식별자는 허용됩니다.

예를 들어 Mockito의 when 함수는 Kotlin에서 사용할 때 백틱이 필요합니다.

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

Any 확장 프로그램 이름 피하기

꼭 필요한 경우가 아니라면 메서드에 Any의 확장 함수 이름을 사용하거나 필드에 Any의 확장 속성 이름을 사용하지 마세요. 멤버 메서드와 필드가 항상 Any의 확장 함수나 속성보다 우선하긴 하지만 코드를 읽을 때 어떤 것이 호출되는지 알기는 어려울 수 있습니다.

null 허용 여부 주석

공개 API의 프리미티브가 아닌 모든 매개변수, 반환 및 필드 유형에는 null 허용 여부 주석이 있어야 합니다. 주석 처리되지 않은 유형은 null 허용 여부가 모호한 '플랫폼' 유형으로 해석됩니다.

기본적으로 Kotlin 컴파일러 플래그는 JSR 305 주석을 준수하지만, 경고와 함께 플래그를 지정합니다. 컴파일러가 주석을 오류로 처리하도록 플래그를 설정할 수도 있습니다.

마지막 람다 매개변수

SAM 변환에 적합한 매개변수 유형은 마지막이어야 합니다.

예를 들어 RxJava 2’s Flowable.create() 메서드 서명은 다음과 같이 정의됩니다.

public static  Flowable create(
    FlowableOnSubscribe source,
    BackpressureStrategy mode) { /* … */ }

FlowableOnSubscribe가 SAM 변환에 적합하므로 Kotlin에서 이 메서드의 함수 호출은 다음과 같습니다.

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

그러나 매개변수가 메서드 서명에서 반대로 되었다면 함수 호출은 trailing-lambda 구문을 사용할 수 있습니다.

Flowable.create(BackpressureStrategy.LATEST) { /* … */ }

속성 접두사

Kotlin에서 메서드를 속성으로 표시하려면 엄격한 'bean' 스타일 접두사를 사용해야 합니다.

접근자 메서드에는 'get' 접두사가 필요하고 부울 반환 메서드에는 'is' 접두사를 사용할 수 있습니다.

public final class User {
  public String getName() { /* … */ }
  public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()

연결된 뮤테이터 메서드에는 'set' 접두사가 필요합니다.

public final class User {
  public String getName() { /* … */ }
  public void setName(String name) { /* … */ }
  public boolean isActive() { /* … */ }
  public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)

메서드를 속성으로 노출하려면 'has'/'set'와 같은 비표준 접두사나 'get'이 아닌 접두사가 지정된 접근자를 사용하지 마세요. 비표준 접두사가 있는 메서드는 메서드의 동작에 따라 허용 가능한 함수로 여전히 호출될 수 있습니다.

연산자 오버로드

Kotlin에서 특수 호출 사이트 구문(즉, 연산자 오버로드)을 허용하는 메서드 이름에 유의하세요. 그러한 메서드 이름이 단축 구문으로 사용하기에 타당한지 확인합니다.

public final class IntBox {
  private final int value;
  public IntBox(int value) {
    this.value = value;
  }
  public IntBox plus(IntBox other) {
    return new IntBox(value + other.value);
  }
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)

Kotlin(자바 사용의 경우)

파일 이름

파일에 최상위 함수 또는 속성이 포함되어 있으면 항상 @file:JvmName("Foo")으로 주석 처리하여 좋은 이름을 제공합니다.

기본적으로 MyClass.kt 파일의 최상위 멤버는 MyClassKt라는 클래스로 끝나며 이는 매력적이지 않고 구현 세부정보로 언어를 유출합니다.

@file:JvmMultifileClass를 추가하여 여러 파일의 최상위 멤버를 단일 클래스로 결합해보세요.

람다 인수

자바에서 사용하도록 의도된 함수 유형은 반환 유형 Unit을 피해야 합니다. 이렇게 하려면 직관적이지 않은 명시적 return Unit.INSTANCE; 구문을 지정해야 합니다.

fun sayHi(callback: (String) -> Unit) = /* … */
// Kotlin caller:
greeter.sayHi { Log.d("Greeting", "Hello, $it!") }
// Java caller:
greeter.sayHi(name -> {
    Log.d("Greeting", "Hello, " + name + "!");
    return Unit.INSTANCE;
});

이 구문은 다른 유형에서 구현될 수 있도록 의미론적으로 이름이 지정된 유형의 제공도 허용하지 않습니다.

Kotlin에서 람다 유형에 이름이 지정된 단일 추상 메서드(SAM) 인터페이스를 정의하면 자바 문제가 해결되지만 Kotlin에서 람다 구문이 사용되지 못합니다.

interface GreeterCallback {
    fun greetName(name: String): Unit
}

fun sayHi(callback: GreeterCallback) = /* … */
// Kotlin caller:
greeter.sayHi(object : GreeterCallback {
    override fun greetName(name: String) {
        Log.d("Greeting", "Hello, $name!")
    }
})
// Java caller:
greeter.sayHi(name -> Log.d("Greeting", "Hello, " + name + "!"))

자바에서 이름이 지정된 SAM 인터페이스를 정의하면 인터페이스 유형을 명시적으로 지정해야 하는 약간 낮은 버전의 Kotlin 람다 구문을 사용할 수 있습니다.

// Defined in Java:
interface GreeterCallback {
    void greetName(String name);
}
fun sayHi(greeter: GreeterCallback) = /* … */
// Kotlin caller:
greeter.sayHi(GreeterCallback { Log.d("Greeting", "Hello, $it!") })
// Java caller:
greeter.sayHi(name -> Log.d("Greeter", "Hello, " + name + "!"));

현재 자바와 Kotlin에서 모두 람다로 사용하는 매개변수 유형을 정의하여 두 언어에서 모두 직관적으로 느껴지도록 하는 방법은 없습니다. 현재 권장사항은 반환 유형이 Unit일 때 자바 환경이 저하되더라도 함수 유형을 선호하는 것입니다.

Nothing 제네릭 피하기

제네릭 매개변수가 Nothing인 유형은 자바에 원시 유형으로 노출됩니다. 원시 유형은 자바에서 거의 사용되지 않으므로 피해야 합니다.

예외 문서화

확인된 예외를 발생시킬 수 있는 함수는 @Throws로 문서화해야 합니다. 런타임 예외는 KDoc에 문서화되어야 합니다.

함수가 위임하는 API에 유의해야 합니다. 다른 경우라면 Kotlin이 자동으로 전파할 수 있는 확인된 예외를 발생시킬 수 있기 때문입니다.

방어적 복사

공개 API에서 공유된 또는 소유되지 않은 읽기 전용 컬렉션을 반환하는 경우 수정 불가능한 컨테이너로 래핑하거나 방어적인 복사를 실행합니다. Kotlin은 읽기 전용 속성을 시행하지만 자바에는 이러한 시행이 없습니다. 래퍼 또는 방어적 복사가 없으면 오래 지속되는 컬렉션 참조를 반환하여 불변을 위반할 수 있습니다.

컴패니언 함수

컴패니언 객체의 공개 함수는 @JvmStatic으로 주석 처리되어 정적 메서드로 노출되어야 합니다.

주석이 없으면 이러한 함수는 정적 Companion 필드의 인스턴스 메서드로만 사용할 수 있습니다.

올바르지 않음: 주석 없음

class KotlinClass {
    companion object {
        fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.Companion.doWork();
    }
}

올바름: @JvmStatic 주석

class KotlinClass {
    companion object {
        @JvmStatic fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.doWork();
    }
}

컴패니언 상수

companion object에서 유효 상수인 const가 아닌 공개 속성은 @JvmField로 주석 처리하여 정적 필드로 노출되어야 합니다.

주석이 없으면 이러한 속성은 정적 Companion 필드에서 이상하게 이름이 지정된 인스턴스 'getters'로만 사용할 수 있습니다. @JvmField 대신 @JvmStatic을 사용하면 이상하게 이름이 지정된 'getters'를 클래스의 정적 메서드로 이동하지만 여전히 올바르지 않습니다.

올바르지 않음: 주석 없음

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
    }
}

올바르지 않음: @JvmStatic 주석

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.getBIG_INTEGER_ONE());
    }
}

올바름: @JvmField 주석

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.BIG_INTEGER_ONE);
    }
}

직관적인 이름 지정

Kotlin에는 함수 이름의 지정 방식을 변경할 수 있는 자바와는 다른 호출 규칙이 있습니다. @JvmName을 사용하여 두 언어의 규칙에 모두 직관적으로 느껴지도록 또는 각 표준 라이브러리 이름 지정과 일치하도록 이름을 디자인합니다.

이 작업은 확장 함수와 확장 속성에 가장 빈번하게 발생합니다. 수신기 유형의 위치가 다르기 때문입니다.

sealed class Optional
data class Some(val value: T): Optional()
object None : Optional()

@JvmName("ofNullable")
fun  T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
    val nullableString: String? = "foo"
    val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
    String nullableString = "Foo";
    Optional optionalString =
          Optionals.ofNullable(nullableString);
}

기본값의 함수 오버로드

기본값이 있는 매개변수가 포함된 함수는 @JvmOverloads를 사용해야 합니다. 이 주석이 없으면 기본값을 사용하여 함수를 호출할 수 없습니다.

@JvmOverloads를 사용할 때는 생성된 메서드를 검사하여 각각 타당한지 확인합니다. 타당하지 않으면 만족할 때까지 다음 리팩터링 중 하나 또는 둘을 모두 실행합니다.

  • 매개변수 순서를 변경하여 끝을 향하고 있는 기본값을 가진 매개변수를 우선합니다.
  • 기본값을 수동 함수 오버로드로 이동합니다.

올바르지 않음: @JvmOverloads 없음

class Greeting {
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Mr.", "Bob");
    }
}

올바름: @JvmOverloads 주석

class Greeting {
    @JvmOverloads
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Bob");
    }
}

린트 검사

요구사항

  • Android 스튜디오 버전: 3.2 Canary 10 이상
  • Android Gradle 플러그인 버전: 3.2 이상

지원되는 검사

이제 Android 린트 검사를 통해 위에 설명된 상호운용성 문제를 감지하고 신고할 수 있습니다. 현재 자바의 문제(Kotlin 사용의 경우)만 감지됩니다. 구체적으로 지원되는 검사는 다음과 같습니다.

  • 알 수 없는 nullness
  • 속성 액세스
  • Kotlin 하드 키워드 없음
  • 마지막 람다 매개변수

Android 스튜디오

이러한 검사를 사용 설정하려면 File > Preferences > Editor > Inspections로 이동하여 Kotlin 상호운용성에 따라 사용 설정하려는 규칙을 확인합니다.

그림 1. Android 스튜디오의 Kotlin 상호운용성 설정

사용 설정하려는 규칙을 확인하면 코드 검사(Analyze > Inspect Code…)를 실행할 때 새로운 검사가 실행됩니다.

명령줄 빌드

명령줄 빌드에서 이러한 검사를 사용 설정하려면 build.gradle 파일에 다음 줄을 추가합니다.

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

lintOptions 내에서 지원되는 전체 구성 세트는 Android Gradle DSL 참조를 읽어보세요.

그런 다음 ./gradlew lint를 명령줄에서 실행합니다.