Kotlin 1.4.30의 새로운 언어 기능 테스트 버전

Read this post in other languages:

Kotlin 1.5에 다음과 같은 언어 기능을 새로 추가할 계획이며, 이 기능은 Kotlin 1.4.30에서 사용해 볼 수 있습니다.

새 기능을 사용하려면 1.5 언어 버전을 지정해야 합니다.

새로운 릴리스 주기에 따라 Kotlin 1.5는 몇달 후에 출시될 예정이지만 새로운 기능은 1.4.30에서 테스트 버전으로 제공됩니다. 조기에 받는 여러분의 의견은 저희에게 매우 소중하므로 지금 새 기능을 평가해주시면 감사하겠습니다.

inline value 클래스 안정화

inline 클래스는 Kotlin 1.3부터 알파 버전으로 제공되었으며 1.4.30에서는 베타 버전으로 승격되었습니다.

Kotlin 1.5에서는 inline 클래스의 개념을 안정화하는 동시에 더 일반적인 기능인 value 클래스의 일부로 만듭니다. 이에 관해서는 이 글의 후반부에서 설명드리겠습니다.

먼저 inline 클래스의 작동 원리를 상기해보죠. inline 클래스에 익숙하시다면 이 섹션을 건너뛰고 새로운 변경 내용을 바로 보시면 됩니다.

간단히 환기해 드리면, inline 클래스는 다음과 같이 값을 감싼 래퍼를 제거합니다.

inline 클래스는 기본 유형(primitive type)이나 참조 유형(예: String)의 래퍼가 될 수 있습니다.

컴파일러는 가능한 경우 inline 클래스 인스턴스(이 예시에서는 Color 인스턴스)를 바이트코드의 기본 유형으로 바꿉니다.

내부적으로 컴파일러는 Int를 매개변수로 사용하여 맹글링된 이름을 가진 changeBackground 함수를 생성하고, 호출 사이트에서 래퍼를 만들지 않고 255 상수를 직접 전달합니다.

이름을 맹글링하면 다른 inline 클래스의 인스턴스를 사용하는 함수를 원활하게 오버로드하고, inline 클래스의 내부 제약 조건에 위반되는 Java 코드의 우발적 호출을 방지할 수 있습니다. 맹글링된 이름의 함수를 Java에서 사용할 수 있게 하는 방법은 아래를 참조하세요.

래퍼가 바이트코드에서 항상 제거되는 것은 아닙니다. 가능한 경우에만 제거되며 기본 제공되는 기본 유형과 매우 유사하게 작동합니다. Color 유형의 변수를 정의하거나 함수에 직접 전달하면 해당 변수가 기본 값으로 대체됩니다.

이 예시에서 color 변수는 컴파일 중에 Color 유형을 갖지만 바이트코드에서 Int로 대체됩니다.

그러나 이 변수를 컬렉션에 저장하거나 제네릭 함수에 전달하면 Color 유형의 일반 객체에 박싱됩니다.

박싱 및 박싱 해제는 컴파일러가 자동으로 수행합니다. 따라서 사용자가 신경 쓸 필요는 없으나 내부의 작동 원리를 이해하면 유용합니다.

Java 호출에 대한 JVM 이름 변경

1.4.30부터 Java에서 사용할 수 있도록 inline 클래스를 매개변수로 삼는 함수의 JVM 이름을 변경할 수 있습니다. 기본적으로 이러한 이름은 Java에서 우발적으로 사용되거나 오버로드가 충돌하는 것을 방지하기 위해 맹글링됩니다(위 예시의 changeBackground-euwHqFQ 참조).

@JvmName으로 함수에 어노테이션을 달면 바이트코드에서 이 함수의 이름을 변경하여 Java에서 호출하고 값을 직접 전달할 수 있습니다.

@JvmName 어노테이션이 달린 함수와 마찬가지로 Kotlin에서는 해당 함수를 Kotlin 이름으로 호출합니다. Timeout 유형의 값만 인수로 전달할 수 있고 단위를 사용 위치에서 분명히 알 수 있기 때문에 Kotlin 사용 위치에서는 유형이 안전합니다.

Java에서 long 값을 직접 전달할 수도 있습니다. 그러나 이렇게 하면 유형이 안전하지 않으므로 기본적으로 작동하지 않습니다. 코드에서 greetAfterTimeout(2)을 보면 2초인지, 2밀리초인지, 2년인지 바로 알기 어렵습니다.

어노테이션을 제공하면 이 함수를 Java에서 호출하도록 의도했다는 것을 명시적으로 강조할 수 있습니다. 설명적인 이름을 사용하면 혼동을 피할 수 있습니다. JVM 이름에 “Millis”라는 접미사를 추가하면 Java 사용자가 단위를 명확하게 알 수 있습니다.

Init 블록

1.4.30에서 inline 클래스에 적용한 또 다른 개선 사항은 init 블록에서 초기화 논리를 정의할 수 있다는 것입니다.

이것은 이전에 금지되었습니다.

inline 클래스에 관한 자세한 내용은 그와 관련된 KEEP, 문서, 이슈 논의에서 확인할 수 있습니다.

inline value 클래스

Kotlin 1.5에서는 inline 클래스의 개념을 안정화하는 동시에 더 일반적인 기능인 value 클래스의 일부로 만듭니다.

지금까지 ‘inline’ 클래스는 별도의 언어 기능이었으나 이제 하나의 매개변수가 있는 value 클래스의 특정 JVM 최적화 요소가 되었습니다. value 클래스는 더 일반적인 개념을 나타내며 향후 다양한 최적화 요소를 지원할 것입니다. 이번에는 inline 클래스를, 다음에 Valhalla 프로젝트를 사용할 수 있게 되면 Valhalla 기본 클래스를 지원할 예정입니다(자세한 내용은 아래 참조).

현재 유일하게 변경된 것은 구문입니다. inline 클래스가 최적화된 value 클래스이기 때문에 다음과 같이 예전과는 다른 방식으로 선언해야 합니다.

하나의 생성자 매개변수로 value 클래스를 정의하고 @JvmInline으로 어노테이션을 추가하세요. Kotlin 1.5부터 이 새로운 구문을 모두가 사용해 주시기를 바랍니다. 이전 구문인 inline 클래스는 한동안 계속 작동합니다. 그러나 1.5에서는 모든 선언을 자동으로 마이그레이션하는 옵션이 포함된 경고가 표시되면서 더 이상 지원되지 않습니다. 추후에는 오류가 표시되며 사용 중지됩니다.

value 클래스

value 클래스는 데이터가 있는 불변 엔티티를 나타냅니다. 현재 value 클래스는 ‘예전’ inline 클래스의 사용 사례를 지원하기 위해 단 하나의 프로퍼티만 포함할 수 있습니다.

향후 Kotlin 버전에서는 이 기능을 완벽하게 지원하여 여러 프로퍼티를 가진 value 클래스를 정의할 수 있습니다. 모든 값은 읽기 전용 val이어야 합니다.

value 클래스에는 동일성(identity)이 없습니다. 저장된 데이터로 완전히 정의되며 === 동일성 확인이 허용되지 않습니다. == 상등 검사는 기본 데이터를 자동으로 비교합니다.

이러한 ‘동일성 없는’ value 클래스의 특성은 향후 중요한 최적화의 기반입니다. 프로젝트 Valhalla 기능을 JVM에서 사용할 수 있게 되면 내부에서 value 클래스를 JVM 기본 클래스로 구현할 수 있습니다.

불변성 제약, 그리고 이에 따른 Valhalla 최적화의 가능성으로 value 클래스는 data 클래스와 차별화됩니다.

향후 Valhalla 최적화

프로젝트 Valhalla는 Java 및 JVM에 새로운 개념인 기본 클래스를 도입합니다.

기본 클래스의 주요 목표는 성능 기본 요소를 일반 JVM 클래스의 객체 지향 이점과 결합하는 것입니다. 기본 클래스는 인스턴스를 변수, 계산 스택에 저장하고 헤더와 포인터 없이 직접 작동할 수 있는 데이터 홀더입니다. 이러한 점에서 int, long 등의 기본 값과 유사합니다(Kotlin에서 사용자가 기본 유형으로 직접 작업할 일이 없지만 이 유형은 컴파일러 내부에서 생성됩니다).

기본 클래스의 주요 이점은 메모리에서 객체의 밀도 있는 플랫 레이아웃을 허용한다는 것입니다. 현재 Array<Point>는 참조 배열입니다. Valhalla가 지원되면 Point를 기본 클래스(Java 용어)로 또는 기본 최적화 기능이 있는 value 클래스(Kotlin 용어)로 정의할 때 JVM은 이를 최적화하고 ‘플랫’ 레이아웃으로 Point 배열을 저장할 수 있습니다(참조의 배열이 아니라 수많은 xy의 직접 배열).

저희는 이러한 예정된 JVM 변경 내용이 구현되기를 기대하고 있으며 Kotlin이 그 혜택을 받기를 바랍니다. 동시에 커뮤니티가 value 클래스를 사용하기 위해 새 JVM 버전을 강제로 사용하기를 바라지 않으므로 이전 JVM 버전에서도 이를 지원할 것입니다. Valhalla를 지원하는 JVM으로 코드를 컴파일할 때 최신 JVM 최적화 기능이 value 클래스에 대해 작동합니다.

mutating 메서드

value 클래스의 기능과 관련하여 얘기할 것이 아직 많이 남았습니다. value 클래스는 ‘불변’ 데이터를 나타내기 때문에 Swift에서와 마찬가지로 mutating 메서드를 적용할 수 있습니다. mutating 메서드는 멤버 함수나 프로퍼티 setter가 기존 인스턴스를 업데이트하지 않고 새 인스턴스를 반환하는 경우이며, 주요 이점은 익숙한 구문으로 다룰 수 있다는 것입니다. 이는 언어에서 여전히 프로토타이핑해야 합니다.

자세한 내용

@JvmInline 어노테이션은 JVM 전용입니다. 다른 백엔드에서는 value 클래스를 다르게 구현할 수 있습니다. 예를 들어 Kotlin/Native에서는 Swift 구조체로 구현됩니다.

value 클래스에 관한 자세한 내용을 알아보려면 Kotlin value 클래스에 관한 설계 노트를 참조하거나 Roman Elizarov의 “A look in the future” 강연의 발췌 내용을 시청하세요.

JVM 레코드 지원

JVM 에코시스템을 위해 마련된 또 다른 개선 사항은 Java 레코드입니다. Java 레코드는 Kotlin data 클래스와 유사하며 주로 단순한 데이터 홀더로 사용됩니다.

Java 레코드는 JavaBeans 규칙을 따르지 않으며 익숙한 ‘getX()’ 및 ‘getY()’ 대신 ‘x()’ 및 ‘y()’ 메서드를 사용합니다.

Java와의 상호운용성은 항상 Kotlin의 우선순위였으며 앞으로도 그럴 것입니다. Kotlin 코드는 새 Java 레코드를 ‘이해’하고 Kotlin 프로퍼티가 있는 클래스로 간주하며, JavaBeans 규칙을 따르는 일반 Java 클래스에서와 마찬가지로 작동합니다.

상호운용성을 주요 목적으로, data 클래스에 @JvmRecord를 어노테이션으로 추가하여 새 JVM 레코드 메서드를 생성할 수 있습니다.

@JvmRecord 어노테이션은 컴파일러가 표준 getX()getY() 메서드 대신 x() and y() 메서드를 생성하도록 합니다. 이 어노테이션은 Java에서 Kotlin으로 변환할 때 클래스의 API를 보존하기 위해서만 사용하면 될 것입니다. 다른 모든 사용 사례에서는 Kotlin의 익숙한 data 클래스를 문제 없이 대신 사용할 수 있습니다.

이 어노테이션은 Kotlin 코드를 JVM 버전 15 이상으로 컴파일하는 경우에만 사용할 수 있습니다. 이 기능에 관한 자세한 내용은 그와 관련된 KEEP, 문서, 이슈 논의에서 확인할 수 있습니다.

sealed 인터페이스 및 sealed 클래스 개선 사항

클래스를 sealed로 만들면 계층 구조를 정의된 하위 클래스로 제한하여 when 브랜치에서 엄격하게 검사할 수 있습니다. Kotlin 1.4에서 sealed 클래스 계층 구조에는 두 가지 제약 조건이 있습니다. 첫째, 최상위 클래스는 sealed 인터페이스가 될 수 없으며 클래스여야 합니다. 둘째, 모든 하위 클래스는 동일한 파일에 있어야 합니다.

Kotlin 1.5에서는 두 가지 제약 조건이 모두 제거되어 이제 인터페이스를 sealed로 만들 수 있습니다. 하위 클래스(sealed 클래스 및 sealed 인터페이스 모두)는 상위 클래스와 동일한 컴파일 유닛 및 동일한 패키지에 있어야 하지만 이제 다른 파일에 있어도 됩니다.

sealed 클래스 및 인터페이스는 추상 데이터 형식(ADT) 계층 구조를 정의하는 데 유용합니다.

sealed 인터페이스로 원활하게 처리할 수 있는 또 다른 주요 사용 사례는 라이브러리 외부에서 상속과 구현을 위해 인터페이스를 닫을 때입니다. 인터페이스를 sealed로 정의하면 해당 구현이 동일한 컴파일 유닛 및 패키지로 제한되므로 라이브러리의 경우 라이브러리 외부에서 구현할 수 없습니다.

예를 들어, kotlinx.coroutines 패키지의 Job 인터페이스는 kotlinx.coroutines 라이브러리 내에서만 구현하도록 의도되어 있습니다. 이 의도는 해당 인터페이스를 sealed로 만들면 명확해집니다.

라이브러리 사용자는 더 이상 자신의 고유한 Job 하위 클래스를 정의할 수 없습니다. 이는 항상 ‘묵시적’으로 제한되어 있었지만 sealed 인터페이스를 사용하면 컴파일러가 이를 공식적으로 금지할 수 있습니다.

향후 JVM 지원 사용

sealed 클래스의 테스트 버전 지원은 Java 15 및 JVM에 도입되었습니다. 앞으로 사용자가 Kotlin 코드를 최신 JVM으로 컴파일하는 경우(이 기능이 안정화되면 JVM 17 이상일 가능성이 높음) sealed 클래스에 대한 안정된 JVM 지원을 제공할 계획입니다.

Java에서는 다음과 같이 지정된 sealed 클래스 또는 인터페이스의 모든 하위 클래스를 명시적으로 나열합니다.

이 정보는 새로운 PermittedSubclasses 속성을 사용하여 클래스 파일에 저장됩니다. JVM은 런타임 시 sealed 클래스를 인식하고 이 클래스가 승인되지 않은 하위 클래스에 의해 확장되지 않도록 합니다.

향후 Kotlin을 최신 JVM으로 컴파일할 때 sealed 클래스에 대한 이 새로운 JVM 지원이 사용됩니다. 내부적으로 컴파일러는 JVM 지원 및 추가 런타임 검사가 있는지 확인하기 위해 바이트코드에서 허용된 하위 클래스 목록을 생성합니다.

Kotlin에서는 하위 클래스 목록을 지정할 필요가 없습니다. 컴파일러가 동일한 패키지에 선언된 하위 클래스를 기반으로 목록을 생성합니다.

상위 클래스 또는 인터페이스의 하위 클래스를 명시적으로 지정하는 기능은 나중에 선택적 사양으로 추가될 수 있습니다. 지금은 필요하지 않다는 판단이지만 여러분의 사용 사례와 이 기능의 필요성에 대해 의견을 주시면 기꺼이 경청하겠습니다.

이전 JVM 버전의 경우 이론적으로는 Java 하위 클래스를 Kotlin sealed 인터페이스에 정의할 수 있지만 그렇게 하지 않는 것이 좋습니다. 허용된 하위 클래스에 대한 JVM 지원은 아직 제공되지 않기 때문에 이 제약 조건은 Kotlin 컴파일러에서만 적용됩니다. 실수로 이 작업이 수행되는 일을 방지하기 위해 IDE에 경고도 추가할 예정입니다. 향후 Java에서 ‘승인되지 않은’ 하위 클래스가 없는지 확인하기 위해 최신 JVM 버전에 새 메커니즘이 사용됩니다.

sealed 인터페이스 및 완화된 sealed 클래스 제한에 관한 자세한 내용은 그와 관련된 KEEP, 문서, 이슈 논의에서 확인할 수 있습니다.

새 기능 사용 방법

이 기능을 사용하려면 Kotlin 1.4.30을 사용해야 합니다. 새 기능을 활성화하려면 다음과 같이 언어 버전을 1.5로 지정하세요.

JVM 레코드를 사용하려면 추가로 jvmTarget 15를 사용하고 JVM 테스트 버전 기능을 활성화해야 합니다. -language-version 1.5-Xjvm-enable-preview 컴파일러 옵션을 추가하세요.

사전 릴리스 노트

새로운 기능에 대한 지원은 실험적이며 1.5 언어 버전 지원은 사전 릴리스 상태입니다. Kotlin 1.4.30 컴파일러에서 언어 버전을 1.5로 설정하면 1.5-M0 테스트 버전을 사용하게 됩니다. 이전 버전과의 호환성은 사전 릴리스 버전에는 적용되지 않습니다. 기능 및 API는 후속 릴리스에서 변경될 수 있습니다. 최종 Kotlin 1.5-RC에 도달하면 사전 릴리스 버전에서 생성된 모든 바이너리가 컴파일러에서 사용 금지되며 1.5-Mx로 컴파일된 모든 항목을 다시 컴파일해야 합니다.

의견을 공유해 주세요

이 글에서 설명한 새로운 기능을 사용해보고 의견을 공유해주세요. KEEP에서 자세한 내용을 확인하고 YouTrack에서 논의에 참여할 수 있으며 문제가 발생하면 새로 보고할 수 있습니다. 새로운 기능이 프로젝트의 사용 사례를 얼마나 잘 처리하는지 여러분의 결과를 공유해 주세요.

추가 읽을거리 및 논의:

이 게시물은 Svetlana Isakova가 작성한 New Language Features Preview in Kotlin 1.4.30를 번역한 글입니다.

image description

Discover more