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를 추가하여 여러 파일의 최상위 멤버를 단일 클래스로 결합해보세요.

람다 인수

자바에서 정의된 단일 메서드 인터페이스 (SAM)는 자연스러운 방식으로 구현을 인라인하는 람다 구문을 사용하여 Kotlin과 자바 모두에서 구현할 수 있습니다. Kotlin에는 이러한 인터페이스를 정의하는 여러 옵션이 있으며, 옵션마다 약간의 차이가 있습니다.

선호되는 정의

자바에서 사용하기 위한 고차 함수Unit를 반환하는 함수 유형을 사용해서는 안 됩니다. 자바 호출자가 Unit.INSTANCE를 반환해야 하기 때문입니다. 서명에 함수 유형을 인라인 처리하는 대신 기능 (SAM) 인터페이스를 사용합니다. 또한 람다로 사용될 것으로 예상되는 인터페이스를 정의할 때 일반 인터페이스 대신 기능 (SAM) 인터페이스를 사용하는 것이 좋습니다. 이렇게 하면 Kotlin에서 직관적으로 사용할 수 있습니다.

다음 Kotlin 정의를 살펴보세요.

fun interface GreeterCallback {
  fun greetName(String name)
}

fun sayHi(greeter: GreeterCallback) = /* … */

Kotlin에서 호출되는 경우:

sayHi { println("Hello, $it!") }

자바에서 호출되는 경우:

sayHi(name -> System.out.println("Hello, " + name + "!"));

함수 유형이 Unit를 반환하지 않더라도 호출자가 람다뿐만 아니라 (Kotlin과 자바 모두에서) 이름이 지정된 클래스로 이를 구현할 수 있도록 이름이 지정된 인터페이스로 만드는 것이 좋습니다.

class MyGreeterCallback : GreeterCallback {
  override fun greetName(name: String) {
    println("Hello, $name!");
  }
}

Unit를 반환하는 함수 유형 피하기

다음 Kotlin 정의를 살펴보세요.

fun sayHi(greeter: (String) -> Unit) = /* … */

다음과 같이 자바 호출자가 Unit.INSTANCE를 반환해야 합니다.

sayHi(name -> {
  System.out.println("Hello, " + name + "!");
  return Unit.INSTANCE;
});

구현에 상태가 있어야 하는 경우 기능 인터페이스를 사용하지 않습니다.

인터페이스 구현이 상태를 보유하도록 되어 있다면 람다 문법을 사용하는 것은 적절하지 않습니다. 비교 가능thisother과 비교하기 위한 것이며 람다에는 this가 없으므로 눈에 띄는 예입니다. 인터페이스 접두사를 fun로 지정하지 않으면 호출자가 object : ... 문법을 사용하게 됩니다. 그러면 상태가 포함될 수 있으며 호출자에게 힌트가 제공됩니다.

다음 Kotlin 정의를 살펴보세요.

// No "fun" prefix.
interface Counter {
  fun increment()
}

Kotlin에서 람다 문법을 방지하므로 다음과 같이 더 긴 버전이 필요합니다.

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

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를 명령줄에서 실행합니다.