2020년 7월 16일
Kotlin 1.4에서는 Java 8 대상 바이트 코드에서 인터페이스의 기본 메서드를 생성하는 새로운 실험적인 방법을 도입하고 있습니다. 나중에는 @JvmDefault 주석을 사용하는 대신 코드를 특수 모드로 컴파일할 때 인터페이스의 모든 메서드 본문을 직접 생성하도록 변경될 것입니다. 현재 동작 방식 및 변경 사항에 대한 자세한 내용은 아래에서 확인할 수 있습니다.
Kotlin에서는 인터페이스에 본문이 있는 메서드를 정의할 수 있습니다. 코드가 JVM에 기본 메서드 지원이 나오기 전인 Java 6 또는 7에서 실행되는 경우에도 작동합니다.
interface Alien {
fun speak() = "Wubba lubba dub dub"
}
class BirdPerson : Alien
더 오래된 Java 버전에서도 이를 작동하게 만들기 위해 Kotlin 컴파일러는 기본 메서드의 구현을 포함하는 추가 클래스를 생성합니다. 이것이 바이트 코드 수준에서의 생성된 코드가 보이는 방식입니다.
public interface Alien {
String speak();
public static final class DefaultImpls {
public static String speak(Alien obj) {
return "Wubba lubba dub dub";
}
}
}
public final class BirdPerson implements Alien {
public String speak() {
return Alien.DefaultImpls.speak(this);
}
}
Kotlin 컴파일러는 speak 메서드를 포함하는 DefaultImpls 클래스를 생성합니다. 이 메서드에는 기본 구현이 포함되어 있습니다. 이 메서드는 인터페이스의 인스턴스를 매개 변수로 사용하고 이를 'this'로 해석하여 내부에서 이 인터페이스의 다른 멤버를 호출할 때 사용됩니다. 인터페이스를 구현하는 BirdPerson 클래스에는 동일한 메서드가 포함되며 실제 'this'를 인수로 전달하면 DefaultImpls의 구현으로 위임합니다.
Kotlin 1.2에서는 Java 8을 대상으로 하는 코드에서 사용할 수 있는 @JvmDefault 주석을 실험적으로 지원하도록 추가했습니다. Java 8을 대상으로 하는 코드에서 기본 구현을 가진 각 인터페이스 메서드에 @JvmDefault 주석을 달 수 있습니다. 이렇게 하면 바이트 코드에서 기본 구현이 생성됩니다.
interface Alien {
@JvmDefault
fun speak() = "Wubba lubba dub dub"
}
class BirdPerson : Alien
이것은 특별한 컴파일러 모드에서만 작동합니다. -Xjvm-default 컴파일러 인수를 지정할 때만 사용할 수 있습니다.
@JvmDefault 주석은 나중에 폐기될 예정입니다. 각 멤버에 주석을 다는 필요가 없으며, 아마도 본문이 있는 모든 인터페이스 메서드에 주석을 다는 것이 필요했으며 상당히 장황했습니다.
최종적으로 Java 8 이상을 대상으로 하는 코드에서는 기본 메서드의 메서드 본문을 기본적으로 생성하고 싶습니다. 이 변경을 빠르게 적용하기는 어렵습니다. 다른 Kotlin 버전과 다른 모드로 컴파일된 응용 프로그램의 라이브러리 또는 모듈을 혼합할 때 문제가 발생하지 않도록 하려고 합니다. 향후 버전의 Kotlin 컴파일러는 기본 메서드의 이전 체계를 계속 "이해"하지만 천천히 새로운 체계로 이동할 것입니다.
인터페이스에서 기본 메서드 생성의 새로운 모드
코드가 Java 8을 대상으로 하고 인터페이스에서 기본 메서드를 생성하려면 Kotlin 1.4에서 두 가지 새로운 모드 중 하나를 사용할 수 있습니다: -Xjvm-default=all 또는 -Xjvm-default=all-compatibility.
all 모드에서는 컴파일러가 기본 메서드만 생성하며 DefaultImpls 객체가 생성되지 않으며 별도의 메서드에 추가 주석을 달 필요가 없습니다. 다음은 초기 예제에 대한 생성된 코드입니다.
// -Xjvm-default=all
public interface Alien {
default String speak() {
return "Wubba lubba dub dub";
}
}
public final class BirdPerson implements Alien {}
인터페이스를 구현하는 BirdPerson 클래스는 speak 메서드를 포함하지 않습니다. JVM 지원 덕분에 "super" 구현을 자동으로 재사용합니다.
새로운 버전의 Kotlin 컴파일러는 이전 체계를 "이해"합니다. 새로운 체계로 컴파일된 클래스가 이전 체계로 컴파일된 인터페이스(기본 DefaultImpls를 사용하는 경우)를 구현하면 컴파일러가 이를 인식하고 해당 DefaultImpls 메서드로 위임하는 숨겨진 메서드를 클래스에 생성합니다.
발생할 수 있는 유일한 문제는 이전 코드를 기본 메서드 구현과 함께 다시 컴파일하고 일부 다른 코드가 이를 의존하는 경우입니다. 이 경우 all-compatibility 모드를 사용하십시오. 그런 다음 기본 메서드 본문과 DefaultImpls 클래스가 모두 생성됩니다.
all-compatibility 모드에서는 이미 인터페이스를 사용하는 클래스를 다시 컴파일할 필요가 없습니다. 정상적으로 작동합니다.
public final class Moopian implements Alien {
public String speak() {
return Alien.DefaultImpls.speak(this);
}
}
all-compatibility 모드는 Kotlin 클라이언트의 이진 호환성을 보장하지만 바이트 코드에서 더 많은 메서드와 클래스를 생성합니다.
대리자 사용 문제 해결
이전에는 @JvmDefault 메서드를 가진 인터페이스를 "대리자로 구현" 기능과 함께 사용하는 것이 약간 혼란스러웠습니다. @JvmDefault가 있는 인터페이스를 대리자로 사용하면 대리자 유형이 고유의 구현을 제공하더라도 기본 메서드 구현이 호출되었습니다.
interface Producer {
fun produce() = "in interface"
}
class ProducerImpl : Producer {
override fun produce() = "in class"
}
class DelegatedProducer(val p: Producer) : Producer by p
fun main() {
val prod = ProducerImpl()
// prints "in interface" if 'produce()' is annotated with @JvmDefault
// prints "in class" in new jvm-default modes
println(DelegatedProducer(prod).produce())
}
새로운 jvm-default 모드를 사용하면 기대한 대로 작동합니다. ProducerImpl 클래스로 구현을 대리자로 사용할 때 재정의된 버전의 produce가 호출됩니다.
@JvmDefaultWithoutCompatibility
all-compatibility 모드로 코드를 컴파일하고 새로운 인터페이스를 추가하는 경우 @JvmDefaultWithoutCompatibility 주석을 사용할 수 있습니다. 이것은이 특정 클래스에 대해 "호환성 모드 없음"(-Xjvm-default=all)를 활성화합니다. 따라서 DefaultImpls 객체가 생성되지 않습니다. 새로운 인터페이스를 추가했기 때문에 이전 체계를 통해 호출되는 코드가 없으므로 아무 문제가 발생하지 않습니다.
더 정확하게 말하면 all-compatibility 모드에서는 공개 이진 인터페이스(ABI로 고려하는 것이 더 정확함)의 일부가 아닌 모든 인터페이스에 @JvmDefaultWithoutCompatibility 주석을 달 수 있으며, 따라서 기존 클라이언트에서 사용되지 않습니다.
라이브러리 작성자를 위한 all-compatibility 모드에 대한 자세한 내용
all-compatibility 모드는 주로 라이브러리 작성자를 위해 설계되었으며 라이브러리의 이진 호환성을 보장하도록 허용합니다. 따라서 다음의 세부 사항과 호환성 문제는 주로 라이브러리 작성자를 대상으로 합니다.
새로운 체계와 이전 체계 간의 이진 호환성을 완전히 "매끄럽게" 보장하는 것은 아닙니다.
발생할 수 있는 호환성 문제를 방지하기 위해 특정한 극단적인 경우에 컴파일러는 오류를 보고하며 @JvmDefaultWithoutCompatibility 주석을 사용하여 이 오류를 억제합니다. 다음 섹션에서는 그 이유와 사용 사례에 대해 설명합니다.
제네릭 인터페이스에서 상속하는 클래스를 고려해보세요.
interface LibGeneric<T> {
fun foo(p: T): T = p
}
open class LibString : LibGeneric<String>
-Xjvm-default=all-compatibility 모드에서 Kotlin 컴파일러는 오류를 생성합니다. 먼저 그 이유를 살펴보고 어떻게 해결할 수 있는지 논의하겠습니다.
내부적으로 이러한 코드가 DefaultImpls 체계로 작동하도록 하려면 이전 버전의 Kotlin 컴파일러(또는 어떤 -Xjvm-default 플래그도 사용하지 않는 경우)는 클래스에 특수화된 시그니처의 추가 메서드를 생성합니다.
open class LibString {
// 자동으로 생성됨:
fun foo(String): String { ... }
}
이러한 특수화된 메서드는 생성된 바이트 코드에서 가끔 호출됩니다. 순수한 Kotlin에서는 이것이 LibString 클래스가 열려 있고 하위 클래스에서 super.foo()를 통해 LibString 인스턴스의 foo를 호출할 때 발생합니다. 혼합된 프로젝트에서 Java를 사용하는 경우 Java에서 이 코드를 사용하면 LibString 인스턴스에서 foo를 호출할 때마다 특수화된 버전이 호출됩니다!
이러한 예기치 않은 NoSuchMethodError 오류가 발생하는 것을 방지하려면 이를 명시적으로 선택하여야 합니다.
컴파일러 오류 수정
가능한 해결 방법 중 하나는 명시적인 재정의를 제공하는 것입니다.
interface LibGeneric<T> {
fun foo(p: T): T = p
}
open class LibString : LibGeneric<String> {
override fun foo(p: String): String = super.foo(p)
}
약간 장황할 수 있지만 그 이유가 있습니다! 이 코드가 Kotlin 하위 클래스에서 사용되거나 Java에서 사용될 수 있는 경우 명시적인 재정의를 추가하면 이전 이진이 새로운 모드로 컴파일된 라이브러리의 새 버전과 함께 이전 바이너리가 계속 작동하는 것을 보장합니다.
다른 옵션은 클래스에 @JvmDefaultWithoutCompatibility 주석을 달아보는 것입니다. 이렇게 하면 명시적 재정의 메서드가 필요하지 않으며 암시적 메서드가 생성되지 않습니다.
interface LibGeneric<T> {
fun foo(p: T): T = p
}
@JvmDefaultWithoutCompatibility
open class LibString : LibGeneric<String> {
// 암시적 멤버 없음
}
부록: 암시적 메서드를 싫어하는 이유
왜 이전 체계처럼 암시적 재정의를 생성하지 않을까요? 다음 다이아몬드 계층을 나타내는 다음 다이어그램을 고려해보세요 - Java 클래스 JavaClass는 Kotlin Base 인터페이스를 KotlinClass를 확장하고 Derived 인터페이스를 구현합니다.
JavaClass().foo()를 호출하면 0이 출력되고 42가 아닙니다! 이것은 0과 42(반환하는 메서드가 두 개뿐임)만 있으므로 Derived에서 더 구체적인 메서드가 호출되지 않는 것이 헷갈리는 새로운 퍼즐이 됩니다. 암시적 메서드인 KotlinClass를 고려하면 결과가 의미가 있습니다. 그러나 암시적 메서드를 처음부터 생성하지 않고 나중에 이러한 퍼즐을 방지하려고 합니다. 필요한 경우 개발자가 명시적 메서드를 제공하도록 하여 이러한 퍼즐을 피하려고합니다.
결론
이전에 @JvmDefault 주석을 사용한 경우 이를 안전하게 제거하고 새로운 모드 중 하나를 사용할 수 있습니다. 이미 -Xjvm-default=enable을 사용한 경우(기본 메서드 구현만 생성함) 이를 -Xjvm-default=all로 바꿀 수 있습니다.
현재 이 지원은 실험적인 상태이지만 향후 주요 Kotlin 버전에서 기본 모드를 계속해서 all-compatibility로 전환할 것입니다. 지금 -Xjvm-default가 지정되지 않으면 생성된 코드는 계속 DefaultImpls를 사용할 것입니다.
어떻게 시도할 수 있는지
이러한 새로운 모드를 이미 Kotlin 1.4-M3 버전으로 시도할 수 있습니다. Kotlin 플러그인을 업데이트하는 방법은 여기에서 확인하세요.
피드백 공유
우리는 이슈 트래커에서 모든 버그 보고에 감사드리며, 최종 릴리스 전에 가장 중요한 문제를 해결하기 위해 최선을 다하겠습니다.
또한 Kotlin Slack의 #eap 채널에 참여하실 것을 환영합니다 (여기에서 초대를 받으세요). 이 채널에서 질문을 하고 토론에 참여하며 새로운 미리보기 빌드 알림을 받을 수 있습니다.
Kotlin을 사용해보세요!
원문
https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/
'Kotlin > Release Notes' 카테고리의 다른 글
[Kotlin Release Notes] Kotlin 1.4.0-RC: Debugging coroutines (0) | 2023.09.10 |
---|---|
[Kotlin Release Notes] Kotlin 1.4.0-RC Released (0) | 2023.09.10 |
[Kotlin Release Notes] Kotlin 1.4-M3 is Out: Standard Library Changes (0) | 2023.09.10 |
[Kotlin Release Notes] Kotlin 1.4-M2 Released (0) | 2023.09.10 |
[Kotlin Release Notes] First Look at Kotlin 1.4-M2: Standard Library Improvements (0) | 2023.09.10 |
댓글