코틀린 1.5.0 버전에서는 새로운 언어적 기능, 안정적인 IR 기반 JVM 컴파일러 백엔드, 성능 향상, experimental 한 기능들에 대한 안정화 같은 점진적인 변화와 이전 내용들에 대한 deprecating 등이 있습니다.
새로운 언어적인 기능
코틀린 1.5.0에서는 1.4.30에서 미리 선보였던 기능들에 대한 안정적인 버전을 제공합니다.
- JVM records 지원
- Sealed 인터페이스와 sealed 클래스 개선사항
- Inline 클래스
JVM records 지원
자바는 아주 빠르게 진화하고 있습니다, 그리고 코틀린에서는 그에 맞춰서 호환을 확실하게 유지하고 있습니다. 우리는 최근에 추가된 기능중 하나인 record 클래스를 지원하기 시작하였습니다.
코틀린은 JVM records 에 양방향 호환을 지원합니다.
- 코틀린 코드에서, 자바의 record 클래스를 일반적인 클래스와 프로퍼티를 사용하는 것처럼 사용할 수 있습니다.
- 자바 코드에서 코틀린 클래스를 record로 사용하고 싶다면, data 클래스를 사용하고 그 위에 @JvmRecord 어노테이션을 붙이면 됩니다.
@JvmRecord
data class User(val name: String, val age: Int)
Sealed 인터페이스
코틀린 인터페이스에서 sealed modifier를 사용할 수 있게 되었습니다. sealed 인터페이스는 sealed 클래스와 같은 방식으로 사용할 수 있고, 모든 구현체는 컴파일 단계에서 확인 가능합니다.
sealed interface Polygon
위와 같은 팩트에 기반하여, when 표현식을 아래와 같이 사용할 수 있습니다.
fun draw(polygon: Polygon) = when (polygon) {
is Rectangle -> // ...
is Triangle -> // …
// else 는 필요없습니다. - 모든 가능한 경우의 수가 covered 됩니다.
}
추가적으로, sealed 인터페이스는 두 개 이상 상속받을 수 있기 때문에, 클래스 계층구조보다 좀 더 유연하게 코드를 작성할 수 있습니다.
class FilledRectangle: Polygon, Fillable
패키지 범위의 sealed 클래스 계층구조
Sealed 클래스들이 이제부터는 같은 패키지 내의 모든 파일에서 서브클래스를 가질 수 있게 되었습니다. 이전에는, 같은 파일 내에서만 서브 클래스를 가질 수 있었습니다.
서브 클래스들은 탑 레벨 클래스일 수 있고, 다른 클래스들, 인터페이스, 객체들 내부에 nested 될수도 있습니다.
sealed 클래스의 서브클래스들은 적절한 이름을 가지고 있어야 합니다. 그들은 로컬이나, 무명 객체가 될 수 없습니다.
Inline 클래스
Inline 클래스들은 오직 value만 가지고 있는 value 기반 클래스들의 부분집합입니다. Inline 클래스들을 사용하여 메모리 할당과 같은 추가적인 오버헤드 없이 특정 타입의 값을 래핑할 수 있습니다.
Inline 클래스들은 클래스 이름 전에 value modifier로 선언할 수 있습니다.
value class Password(val s: String)
JVM 백엔드의 경우 @JvmInline 어노테이션을 필요로 합니다.
@JvmInline
value class Password(val s: String)
Inline modifier를 사용할 경우 이제부터 deprecated warning이 발생하게 됩니다.
코틀린/JVM
코틀린/JVM에서는 내부적인 것들이나 유저에게 영향을 주는 여러 개선사항들이 있었습니다. 아래 사항들이 제일 주목할 만한 것들입니다.
- 안정적인 JVM IR 백엔드
- default JVM 타겟 : 1.8
- invokedynamic을 통한 SAM adapters
- invokedynamic을 통한 람다
- @JvmDefault, 예전 Xjvm-default 모드에 대한 Deprecation
- nullability 어노테이션을 다루는 것에 대한 개선사항
안정적인 JVM IR 백엔드
코틀린/JVM 컴파일러를 위한 IR 기반의 백엔드가 이제 안정적인 상태과 되었고, default로 enabled되었습니다.
코틀린 1.4.0부터, IR 기반의 백엔드는 미리 사용해볼 수 있었고, 이제 1.5 버전이 되면서 default 가 되었습니다. 이전 백엔드는 이전 버전에서 아직 default 값으로 사용되고 있습니다.
코틀린 1.5.0에서 이전 백엔드를 사용하고 싶다면, 프로젝트 설정 파일에 아래와 같은 값을 추가하면 됩니다.
Gradle
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile> {
kotlinOptions.useOldBackend = true
}
Maven
<configuration>
<args>
<arg>-Xuse-old-backend</arg>
</args>
</configuration>
default JVM 타겟 : 1.8
코틀린/JVM의 default target 버전이 1.8이 되었습니다. 1.6 target은 deprecated되었습니다.
만약 JVM 1.6으로 빌드하고 싶다면 아직 target을 변경할 수 있습니다.
invokedynamic을 통한 SAM adapters
kotlin 1.5.0에서는 SAM 변환을 컴파일 할 때 dynamic invocation을 사용합니다.
- SAM 타입이 자바 인터페이스라면 어떤 표현식이든 가능합니다.
- SAM 타입이 코틀린 함수형 인터페이스라면 람다가 가능합니다.
LambdaMetafactory.metafactory()를 사용한 새로운 구현체와 보조 래핑 클래스들은 더 이상 컴파일 과정에서 생성하지 않아도 됩니다. 이것 때문에 애플리케이션 JAR의 크기가 줄어들었고, JVM startup 성능이 좋아졌습니다.
무명 클래스 생성에 기반한 예전 구현체 스키마로 롤백하고 싶다면 컴파일러 옵션에 -Xsam-conversions=class 옵션을 주면 됩니다.
invokedynamic을 통한 람다
invokedynamic을 통하여 평범한 코틀린 람다를 컴파일 하는건 experimental한 기능으로, 언제든지 없어지고 변경될 수 있습니다.
코틀린 1.5.0에서는 평범한 코틀린 람다 ( 함수형 인터페이스 인스턴스로 변환되지 않은 )에 대해 dynamic invocation을 이용하여 experimental 한 지원기능이 생겼습니다. 새로운 구현체는 LambdaMetafactory.metafactory()를 사용하여 더 가벼운 바이너리를 생산해서 런타임에 필요한 클래스들을 효율적으로 생성합니다. 현재는 일반적인 람다 컴파일과 비교했을 때 세 가지 제약사항이 있습니다.
- invokedynamic으로 컴파일된 람다는 serializable하지 않습니다.
- 람다에서 toString()을 부르면 string 표현의 가독성이 떨어집니다.
- LambdaMetafactory로 생성된 람다에서는 Experimental reflect API가 지원되지 않습니다.
@JvmDefault와 Xjvm-default 모드에 대한 Deprecation
코틀린 1.4.0이전에는, -Xjvm-default=enable과 -Xjvm-default=compatibility 모드와 함께 @JvmDefault 가 있었습니다. 그것들은 코틀린 인터페이스 내부에서 추상적이지 않은 멤버들에 대해 JVM default 메소드를 생성하는 역할을 맡고 있었습니다.
코틀린 1.4.0에서는, 전체 프로젝트에서 default 메소드 생성으로 변환시켜주는 Xjvm-default 모드가 생겼습니다.
코틀린 1.5.0에서는 @JvmDefault, 예전 -Xjvm-default 모드인 -Xjvm-default=enable과 -Xjvm-default=compatibility가 deprecating 되었습니다.
nullability 어노테이션을 다루는 것에 대한 개선사항
코틀린은 자바의 nullability 어노테이션들로부터 nullability 타입 정보를 핸들링 하는 것을 지원하고 있습니다. 코틀린 1.5.0에서는 이 기능에 대해 여러 개선사항들이 있었습니다.
- 의존성에 있는 자바 라이브러리에서 컴파일된 타입 아규먼트의 nullability 어노테이션도 읽습니다.
- nullability 어노테이션의 TYPE_USE 타겟을 지원합니다.
- Arrays
- Varargs
- Fields
- 타입 파라미터와 그것의 경계
- 타입 아규먼트의 기반 클래스와 인터페이스
- 만약 nullability 어노테이션이 여러개의 수용가능한 타겟을 가지고 있고, 해당 타겟들중 하나가 TYPE_USE 라면, TYPE_USE가 선호됩니다.
예를 들어서, 메소드 시그니처 @Nullable String[] f()는 @Nullable이 TYPE_USE와 METHOD를 타겟으로 둘 다 지원할 때
fun f(): Array<String?>! 가 됩니다.
새롭게 지원되는 케이스에서, 코틀린에서 자바를 호출할 때 잘못된 nullability 타입을 사용하면 warning이 발생합니다. -Xtype-enhancement-improvements-strict-mode 컴파일러 옵션을 사용해서 이러한 경우 error를 발생시키도록 strict 모드를 활성화 할 수 있습니다.
표준 라이브러리
표준 라이브러리에서는 experimental한 기능을 안정화한것에 따라 몇 가지 변화와 개선사항이 있었습니다.
- 안정적인 unsigned Integer 타입
- 안정적인 locale-구분없는 API ( uppercase/lowercase text )
- 안정적인 Char-to-Integer 변환 API
- 안정적인 Path API
- Floored division과 나머지 연산
- Duration API 변화
- char 카테고리를 가져오는 새로운 API가 멀티플랫폼 코드에서 사용할 수 있게 되었습니다.
- 새로운 Collection의 firstNotNullOf() 함수
- String?.toBoolean()의 엄격한 버전
안정적인 unsigned Integer 타입
UInt, ULong, UByte, UShort unsigned Integer 타입이 안정적으로 변했습니다. 이 타입들에 대한 연산자, 범위, 진행률도 동일하게 안정적으로 변했습니다. Unsigned 배열과 연산자의 경우 베타로 남아있습니다.
안정적인 locale-구분없는 API ( upper/lowercasing text )
이번 릴리즈에서는 새로운 위치에 구분되어지지 않는 text uppercase/lowercase 변환 API가 추가되었습니다. toLowerCase(), toUpperCase(), capitalize(), 그리고 decapitalize() API 와 같이 위치에 영향을 받는 함수들의 대안으로 제공된 API 입니다.
새로운 API는 다른 위치 셋팅 때문에 에러가 발생하는걸 피할 수 있습니다.
코틀린 1.5.0은 완전하게 안정적인 대안 함수들을 제공합니다.
- String 함수
- Char 함수
코틀린/JVM에는 uppercase(), lowercase() 그리고 titlecase() 함수에 명시적 Locale 파라미터를 가지고 있는 오버로딩 함수들도 있습니다.
예전 API 함수들은 deprecated 마킹이 되었고, 미래 릴리즈에서 지워질 예정입니다.
안정적인 Char-to-Integer 변환 API
코틀린 1.5.0으로 오면서, 새로고 안정적인 char-to-code와 char-to-digit 변환 함수들이 추가되었습니다. 이 새로운 함수들은 String-to-Int와 유사한 면을 띄면서 혼동을 주는 기존 API 함수들을 대체합니다.
새로운 API는 함수의 역할을 더욱 투명하고, 덜 모호하게 만듬으로서 혼동을 제거하고, 더 명확하게 코드를 작성할 수 있게 하였습니다.
이번 릴리즈에서는 기존의 Char 변환 을 두 개의 명확하게 이름지어진 함수로 나누었습니다.
- Char의 Integer code를 가져오는 함수 및 주어진 코드로 Char를 만들어 주는 함수
fun Char(code: Int): Char fun Char(code: UShort): Char val Char.code: Int
- Char를 Character가 표현하는 숫자의 숫자값으로 변환해주는 함수 ( '2' -> 2 )
fun Char.digitToInt(radix: Int): Int fun Char.digitToIntOrNull(radix: Int): Int?
- 양수를 받아서 그 숫자에 호환되는 Char 표현으로 변환해주는 Int의 확장함수
fun Int.digitToChar(radix: Int): Char
기존 변환 API, Number.toChar() 와 그것의 구현체 ( Int.toChar()를 제외하고 ) 그리고 Char.toInt()와 같이 Char에서 숫자 타입으로 변환해주는 확장함수들은 이제 deprecated 되었습니다.
안정적인 Path API
experimental 했던 java.nio.file.Path의 확장 Path API가 이제 안정적으로 변경되었습니다.
// construct path with the div (/) operator
val baseDir = Path("/base")
val subDir = baseDir / "subdirectory"
// list files in a directory
val kotlinFiles: List<Path> = Path("/home/user").listDirectoryEntries("*.kt")
Floored division 과 나머지 연산
Floored division : ( 나눗셈을 한 뒤 소수를 버리고 정수만 가져오는 것 ) ex) 5, 2 의 Floored division = 2
새로운 모듈식 산수 연산자가 표준 라이브러리에 추가되었습니다.
- floorDiv() 함수는 floored division에 대한 결과를 돌려줍니다.
- mod() 함수는 floored division의 나머지를 돌려줍니다.
이 연산자들은 기존의 Integer 나눗셈이나, rem() 함수, % 연산자 등과 꽤 비슷하다고 생각할 수 있지만, 음수에서 다르게 동작합니다.
- a.floorDiv(b)는 절삭 ( round down ) 하고 결과적으로 Integer가 더 작은 수가 되지만, / 같은 경우 Integer가 0에 가까이 간다는 점에서 다릅니다.
- a.mod(b)는 0이거나, b와 같은 부호를 가지는 반면, a % b는 다를 수 있습니다.
println("Floored division -5/3: ${(-5).floorDiv(3)}")
println( "Modulus: ${(-5).mod(3)}")
println("Truncated division -5/3: ${-5 / 3}")
println( "Remainder: ${-5 % 3}")
Floored division -5/3: -2
Modulus: 1
Truncated division -5/3: -1
Remainder: -2
Duration API 변화
Duration API는 Experimental 합니다. 언제든지 변하거나 없어질 수 있습니다.
다른 시간 unit들에서 Duration을 표현해주는 Duration 클래스가 있습니다. 1.5.0에서는 Duration API 에 몇 가지 변화가 있었습니다.
- 현재를 나타내는 내부 값을 더 간결하게 표현하기 위해서 Double 대신에 Long으로 변경하였습니다.
- Long에서 특정 타임 unit으로 변환하는 새로운 API가 추가되었습니다. Double 값을 사용하고 deprecated된 기존 연산ㅇ르 대체하는 연산입니다.
ex) Duration.inWholeMinutes는 Duration.inMinutes를 대체하여, duration을 Long으로 표현한 값을 돌려줍니다.
val duration = Duration.milliseconds(120000)
println("There are ${duration.inWholeSeconds} seconds in ${duration.inWholeMinutes} minutes")
There are 120 seconds in 2 minutes
char 카테고리를 가져오는 새로운 API가 멀티플랫폼 코드에서 사용 가능합니다
코틀린 1.5.0에서는 멀티플랫폼 프로젝트에서 유니코드를 통해서 character의 카테고리를 가져오는 새로운 API가 추가되었습니다.
char가 letter인지 digit인지 체크하는 함수입니다.
- Char.isDigit()
- Char.isLetter()
- Char.isLetterOrDigit()
val chars = listOf('a', '1', '+')
val (letterOrDigitList, notLetterOrDigitList) = chars.partition { it.isLetterOrDigit() }
println(letterOrDigitList) // [a, 1]
println(notLetterOrDigitList) // [+]
char의 case를 체크하는 함수입니다.
- Char.isLowerCase()
- Char.isUpperCase()
- Char.IsTitleCase()
val chars = listOf('Dž', 'Lj', 'Nj', 'Dz', '1', 'A', 'a', '+')
val (titleCases, notTitleCases) = chars.partition { it.isTitleCase() }
println(titleCases) // [Dž, Lj, Nj, Dz]
println(notTitleCases) // [1, A, a, +]
다른 함수들입니다.
- Char.isDefined()
- Char.isISOControl()
유니코드를 통한 일반적인 char의 카테고리를 가리키는 Char.category 프로퍼티와 그것의 리턴 타입인 CharCategory enum 클래스는 이제 멀티플랫폼 프로젝트에서 사용가능합니다.
새로운 collections 함수 firstNotNullOf()
새로운 mapNotNull()과 first() 혹은 firstOrNull()이 결합된 firstNotNullOf()와 firstNotNullOfOrNull() 함수가 추가되었습니다.
이 함수들은 커스텀 seletor 함수와 함께 original 콜렉션에서 첫 번째로 not-null인 값을 돌려줍니다. 만약 값이 없다면 firstNotNullOf()는 exception을 throw하고, firstNotNullOfOrNull()은 null을 돌려줍니다.
val data = listOf("Kotlin", "1.5")
println(data.firstNotNullOf(String::toDoubleOrNull))
println(data.firstNotNullOfOrNull(String::toIntOrNull))
1.5
null
String?.toBoolean()의 Strict 버전
기존의 Stirng?.toBoolean()에서 case 에 영향을 받는 두 가지 strict 버전의 새로운 함수가 추가되었습니다.
- String.toBooleanStrict()는 true, false 문자열을 제외한 입력이 들어오면 excpetion을 throw합니다.
- String.toBooleanStrictOrNull()은 true, false 문자열을 제외한 입력이 들어오면 Null을 리턴합니다.
println("true".toBooleanStrict())
println("1".toBooleanStrictOrNull())
// println("1".toBooleanStrict()) // Exception
true
null
kotlin-test 라이브러리
kotlin-test 라이브러리에서 몇 가지 새로운 기능이 추가되었습니다.
- 멀티플랫폼 프로젝트에서의 test 의존성 간결화
- 코틀린/JVM 소스 셋을 위한 테스트 프레임워크 자동 선택
- Assertion 함수 업데이트
멀티플랫폼 프로젝트에서의 test 의존성 간결화
이제부터, commonTest source 내부에 테스트를 위해서 kotlin-test 의존성을 추가하게 되면, Gradle 플러그인이 각 test source 셋에 상호작용하는 플랫폼 의존성을 추론해줄 것입니다.
- JVM source 셋에는 kotlin-test-junit
- 코틀린/JS source 셋에는 kotlin-test-js
- common한 source 셋에는 kotlin-test-common과 kotlin-test-annotations-common
- Kotlin/Native를 위한 source 셋 artifact는 없습니다.
추가적으로, kotlin-test 의존성을 어디에서나 공유할 수 있고, 특정 플랫폼 source 셋에 사용할 수 있습니다.
명시적으로 의존성이 설정된 기존의 kotlin-test의 경우 Gradle, Maven에서 계속 동작할 예정입니다.
코틀린/JVM source 셋에서의 테스트 프레임워크 자동 선택
Gradle 플러그인은 이제부터 자동적으로 테스트 프레임워크 의존성을 골라 추가합니다.
당신이 해야할 것은 kotlin-test 의존성을 common source 셋에 추가하는 일 뿐입니다.
Gradle은 JUnit 4를 default 값으로 사용합니다. 그러므로, kotlin("test") 의존성은 JUnit 4에 대한 변형인 kotlin-test-junit을 선택합니다.
Groovy
kotlin {
sourceSets {
commonTest {
dependencies {
implementation kotlin("test") // This brings the dependency
// on JUnit 4 transitively
}
}
}
}
JUnit 5 나 TestNG의 경우 useJUnitPlatform()이나 useTestNG()를 test task에서 호출하면 됩니다.
tasks {
test {
// enable TestNG support
useTestNG()
// or
// enable JUnit Platform (a.k.a. JUnit 5) support
useJUnitPlatform()
}
}
만약 자동적으로 테스트 프레임워크를 선택하는것을 disable하고 싶다면, gradle.properties에 kotlin.test.infer.jvm.variant=false로 설정하면 됩니다.
Assertion 함수 업데이트
이번 릴리즈에서는 새로운 assertion 함수 추가와 기존 함수들에 대한 개선사항들이 있었습니다.
kotlin-test 라이브러리에서는 이제 아래와 같은 기능을 가집니다.
- 타입 체크 기능
새로운 assertIs<T> 와 assertIsNot<T> 를 통해 타입을 체크할 수 있습니다.
@Test fun testFunction() { val s: Any = "test" assertIs<String>(s) // throws AssertionError mentioning the actual type of s if the assertion fails // can now print s.length because of contract in assertIs println("${s.length}") }
주의 : 타입이 소거되기 때문에, 이 assert 함수는 값이 List 타입인지 여부를 검증할 수 있으며, List<String> 타입인지는 검증할 수 없습니다.
assertIs<List<String>>(value) --> 검증 불가능 - 배열, 시퀀스, 산술 iterable에 대한 내용 비교
다른 콜렉션에 대한 내용을 비교하는 assertContentEquals() 함수들이 추가되었습니다.
@Test fun test() { val expectedArray = arrayOf(1, 2, 3) val actualArray = Array(3) { it + 1 } assertContentEquals(expectedArray, actualArray) }
- 새로운 Double, Float 숫자에 대한 assertEquals(), assertNotEquals() 오버로딩 함수
Double, Float 숫자들의 정밀도 비교를 가능하게 하기 위해서 assertEquals()의 새로운 오버로딩 함수들을 추가하였습니다. 정밀도 값의 경우 세 번째 파라미터로 받습니다.
@Test fun test() { val x = sin(PI) // precision parameter val tolerance = 0.000001 assertEquals(0.0, x, tolerance) }
- 콜렉션의 내용과 element를 체크하는 새로운 함수
콜렉션이나 element에 무언가가 contains 되었는지 여부를 체크할 수 있는 assertContains() 함수가 추가되었습니다. 이를 코틀린 컬렉션이나 contains() 연산자가 있는 IntRange, String 등에 사용할 수 있습니다.
@Test fun test() { val sampleList = listOf<String>("sample", "sample2") val sampleString = "sample" assertContains(sampleList, sampleString) // element in collection assertContains(sampleString, "amp") // substring in string }
- assertTrue(), assertFalse(), expect() 함수들이 inline 함수가 되었습니다.
지금부터, 이 함수들은 inline 함수들이 되었습니다. 그러므로, 람다 표현식 내부에서 suspend 함수를 호출할 수 있습니다.
@Test fun test() = runBlocking<Unit> { val deferred = async { "Kotlin is nice" } assertTrue("Kotlin substring should be present") { deferred.await() .contains("Kotlin") } }
출처
'Kotlin' 카테고리의 다른 글
코틀린 1.4, 1.5 버전에서의 변경사항 (0) | 2021.08.15 |
---|---|
(번역글) kotlin 1.5.20 릴리즈 노트 (0) | 2021.08.15 |
(번역글) 코틀린 1.4.30 릴리즈 노트 (0) | 2021.08.12 |
(번역글) 코틀린 1.4.20 버전 릴리즈 노트 (0) | 2021.08.11 |
(번역글) 코틀린 1.4.0 버전 변경 사항 (0) | 2021.07.24 |