큰 변화는 다음과 같습니다.
- JVM 19 지원
- 컴파일러 최적화 비활성화 옵션 제공
- Lombok
@Builder
어노테이션 지원 - Gradle 7.2 & 7.3 지원
- Kotlin Standard Library 개선
- kotlin-reflect 성능 개선
Kotlin/JVM
JVM 19 지원
1.8.0 부터 코틀린 컴파일러는 JVM 버전 19를 지원합니다. JVM 19에 맞춘 바이트코트 클래스를 생산할 수 있다는 말이죠.
컴파일러 최적화 비활성화 옵션 제공
1.8.0 부터는 컴파일러 최적화 옵션을 비활성화하는 -Xdebug
옵션을 제공합니다. 디버깅을 조금 더 쉽게 하기 위한 옵션입니다. 정확히는 컴파일러의 was optimized out
기능을 끄는 옵션인데요, 해당 기능이 어떤 역할을 하는지에 대해서는 내용이 길어질 것 같아 별도의 글로 설명하겠습니다. 이 옵션이 당장은 해당 기능만 비활성화하지만 추후에 컴파일러에 다른 최적화 기능이 추가된다면 해당 기능 또한 비활성화 될 겁니다.
⚠️ 프로덕션 환경에서는 절대 사용하지 마세요!
-Xdebug
옵션을 켜는 것은 심각한 memory leak 을 유발합니다.
old backend 제거
1.5.0 에서 IR 기반 backend가 stable 단계에 접어들면서 old backend는 deprecated 됐었는데요. 1.8.0 부터 old backend는 완전히 사라집니다. 때문에 자연스럽게 Xuse-old-backend
컴파일러 옵션과 useOldBackend
gradle 옵션도 삭제되었습니다.
Lombok @Builder
어노테이션 지원
코틀린에 Lombok의 @Builder 어노테이션을 지원해달라는 요구가 굉장히 많았습니다. 1.8.0 부터 해당 어노테이션을 지원합니다.
Lombok을 사용하려면 아래처럼 gradle 플러그인을 추가해야합니다.
plugins {
kotlin("plugin.lombok") version "1.8.0"
id("io.freefair.lombok") version "5.3.0"
}
Gradle 7.2 & 7.3 지원
1.8.0 부터 gradle 7.2와 7.3 버전을 완전히 지원하며 gradle 최소 지원 버전은 6.8.3 입니다.
compile task 간의 JVM target 검사 강제화
1.8.0 부터 kotlin.jvm.target.validation.mode
의 기본값이 error
로 변경됩니다.
코드 빌드시에 코틀린 gradle 플러그인은 JVM target 호환성을 검사합니다. 가령, compileKotlin
task의 jvmTarget
값과 compileJava
task의 targetCompatibility
값을 비교하고 호환성을 검사하는거죠. 만약 위 값들이 달라서 호환성에 문제가 생길 경우 컴파일러가 어떻게 대처할 지 그 행동을 지정하는 옵션이 build.gradle
의 kotlin.jvm.target.validation.mode
값 입니다. 옵션은 아래처럼 3가지가 존재합니다.
기존에는 warning
이었고 이제 error
으로 변경됩니다. 이제 JVM target 이 호환되지 않을 경우 빌드는 실패하겠죠.
기본 값을 이렇게 변경한 이유는 Gradle 8.0 으로 마이그레이션을 조금 더 쉽게 하기위함이라고 합니다.
Standard Library
JVM compilation target 상향
1.8.0 부터 코틀린 표준 라이브러리(kotlin-stdlib
, kotlin-reflect
, kotlin-script-*
)가 JVM 1.8로 컴파일됩니다. 이전까지 표준 라이브러리들은 JVM 1.6으로 컴파일 되었습니다.
또한, 코틀린 1.8.0 부터는 JVM 1.6과 1.7을 더이상 지원하지 않습니다. 그래서 이제 빌드 스크립트에서 kotlin-stdlib-jdk7
이나 kotlin-stdlib-jdk8
를 따로 선언해 줄 필요가 없습니다. 이 둘은 kotlin-stdlib
로 통합되었습니다.
cbrt()
double 이나 float 의 세제곱근을 계산하는 cbrt()
메소드가 이제 stable 단계입니다.
import kotlin.math.*
fun main() {
val num = 27
val negNum = -num
println("The cube root of ${num.toDouble()} is: "
+ cbrt(num.toDouble()))
// The cube root of 27.0 is: 3.0
println("The cube root of ${negNum.toDouble()} is: "
+ cbrt(negNum.toDouble()))
// The cube root of -27.0 is: -3.0
}
Java와 Kotlin 사이의 TimeUnit 변환
kotlin.time
패키지의 toTimeUnit()
, toDurationUnit()
메소드가 이제 stable 단계입니다. 코틀린 1.6.0 에서 실험적으로 발표된 메소드들인데요, 코틀린과 자바 사이의 상호 운용성을 위해 만들어졌습니다. 이 메소드들을 사용하면 java.util.concurrent.TimeUnit
과 kotlin.time.DurationUnit
사이의 변환을 손쉽게 수행할 수 있습니다.
import kotlin.time.*
// For use from Java.
fun wait(timeout: Long, unit: TimeUnit) {
val duration: Duration = timeout.toDuration(unit.toDurationUnit())
...
}
직접 연산 가능한 TimeMark 클래스
⚠️ TimeMark 클래스의 새로운 기능은 아직 Experimental 단계입니다. 이 메소드를 사용하려면
@OptIn(ExperimentalTime::class)
이나@ExperimentalTime
어노테이션 선언이 필요합니다.
1.8.0 이전에는 여러 TimeMark
사이의 시간 차를 얻기위해 각각의 TimeMark
에 elapsedNow()
를 호출해서 그 차이를 계산했습니다. 하지만 이 결과는 다소 부정확하고 비결정적이었는데요. 여러 elapsedNow()
메소드의 호출 사이 사이에 시간이 얼마나 지나갔을 지 모르기 때문입니다. elapsedNow()
메소드를 수행하는 데 걸리는 시간은 계산할 수가 없는겁니다.
이 문제를 해결하기 위해 코틀린 1.8.0 부터는 각각의 TimeMark
를 직접 비교하거나 연산이 가능합니다. elapsedNow()
메소드를 쓸 필요 없이 그냥 TimeMark
인스턴스 끼리 빼거나 비교하면 됩니다.
import kotlin.time.*
@OptIn(ExperimentalTime::class)
fun main() {
val timeSource = TimeSource.Monotonic
val mark1 = timeSource.markNow()
Thread.sleep(500) // 0.5초 sleep
val mark2 = timeSource.markNow()
// 1.8.0 이전 방식
repeat(4) { n ->
val elapsed1 = mark1.elapsedNow()
val elapsed2 = mark2.elapsedNow()
// elapsed1과 elapsed2 사이의 차이는 반복문이 4번 도는 동안 모두 다릅니다.
// 두 연속적인 elapsedNow() 메소드 호출 사이에 시간이 얼마나 지날지 모르기 때문입니다.
println("Measurement 1.${n + 1}: elapsed1=$elapsed1, " +
"elapsed2=$elapsed2, diff=${elapsed1 - elapsed2}")
}
println()
// 1.8.0 이후 방식
repeat(4) { n ->
val mark3 = timeSource.markNow()
// TimeMark 끼리 연산이 가능
val elapsed1 = mark3 - mark1
val elapsed2 = mark3 - mark2
// 이제 elapsed1과 elapsed2 사이의 차이는 확정적입니다.
println("Measurement 2.${n + 1}: elapsed1=$elapsed1, " +
"elapsed2=$elapsed2, diff=${elapsed1 - elapsed2}")
}
// TimeMark 끼리 비교(compare) 가능
// 결과는 true 여야합니다. mark2가 mark1 보다 500ms 이후에 생성되었기 때문이죠.
println(mark2 > mark1)
}
위 코드를 실행한 결과는 아래와 같습니다.
측정 1.1: elapsed1=505.379166ms, elapsed2=25.499875ms, diff=479.879291ms
측정 1.2: elapsed1=553.414375ms, elapsed2=48.245625ms, diff=505.168750ms
측정 1.3: elapsed1=553.527875ms, elapsed2=48.361791ms, diff=505.166084ms
측정 1.4: elapsed1=553.616833ms, elapsed2=48.441291ms, diff=505.175542ms
측정 2.1: elapsed1=553.692625ms, elapsed2=48.514500ms, diff=505.178125ms
측정 2.2: elapsed1=553.942250ms, elapsed2=48.764125ms, diff=505.178125ms
측정 2.3: elapsed1=554.005916ms, elapsed2=48.827791ms, diff=505.178125ms
측정 2.4: elapsed1=555.465541ms, elapsed2=50.287416ms, diff=505.178125ms
true
측정 1번은 elapsedNow()
메소드 호출때문에 4번 모두 결과가 다르지만 측정 2번은 TimeMark
인스턴스를 직접 연산하기 때문에 결과가 모두 같은걸 볼 수 있습니다.
이 기능은 프레임 사이의 차이를 계산하고 비교할 일이 많은 애니메이션 분야에 특히나 유용합니다.
💡 kotlin.time 패키지는 별도의 디펜던시를 추가해야 사용할 수 있습니다.
implementation*("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
디렉토리 순환 복사 & 삭제(Recursive copying or deletion of directories)
⚠️ java.nio.file.Path 클래스의 새로운 기능은 아직 Experimental 단계입니다. 이 메소드를 사용하려면
@OptIn(kotlin.io.path.ExperimentalPathApi::class)
이나@kotlin.io.path.ExperimentalPathApi
어노테이션 선언이 필요합니다. 컴파일러 옵션에-opt-in=kotlin.io.path.ExperimentalPathApi
를 선언하셔도 됩니다.
java.nio.file.Path
클래스에 두가지 새로운 확장함수가 추가되었습니다. copyToRecursively()
메소드와 deleteRecursively()
메소드입니다. 각각은 다음과 같은 기능을 수행합니다.
copyToRecursively()
- 디렉토리와 내부의 콘텐츠를 다른 디렉토리로 복사합니다.
deleteRecursively()
- 디렉토리와 내부의 콘텐츠를 삭제합니다.
Error handling
copyToRecursively()
메소드는 내부에 onError
람다 함수를 오버로드해서 Exception
이 발생했을 때의 핸들링 로직을 작성할 수 있습니다.
sourceRoot.copyToRecursively(destinationRoot, followLinks = false,
onError = { source, target, exception ->
logger.logError(exception, "Failed to copy $source to $target")
OnErrorResult.TERMINATE
})
deleteRecursively()
수행 도중 Exception
이 발생하면 해당 파일이나 폴더는 생략되고 삭제 작업이 끝난 이후 작업 도중 발생한 모든 Exception
을 포함한 IOException
을 던집니다.
File overwrite
copyToRecursively()
수행 도중 destination 디렉토리에 중복 파일이 발견되면 Exception
이 발생합니다. 파일을 덮어쓰고 싶다면 overwrite
인자를 true
로 설정하면 됩니다.
fun setUpEnvironment(projectDirectory: Path, fixtureName: String) {
fixturesRoot.resolve(COMMON_FIXTURE_NAME)
.copyToRecursively(projectDirectory, followLinks = false)
fixturesRoot.resolve(fixtureName)
.copyToRecursively(projectDirectory, followLinks = false,
overwrite = true) // patches the common fixture
}
Custom copying action
copyAction
을 오버로드하면 커스텀 복사 로직을 수행할 수 있습니다.
sourceRoot.copyToRecursively(destinationRoot, followLinks = false) { source, target ->
if (source.name.startsWith(".")) {
CopyActionResult.SKIP_SUBTREE
} else {
source.copyToIgnoringExistingDirectory(target, followLinks = false)
CopyActionResult.CONTINUE
}
}
위 메소드에 대한 더 자세한 정보는 API 문서를 참조해주세요.
Java Optional 클래스 확장 함수
1.7.0 에서 Optional 클래스의 확장 함수들(getOrNull()
, getOrDefault()
, getOrElse()
)이 소개됐었죠. 자바 Optional 클래스를 조금 더 쉽게 다룰 수 있도록 도와주는 확장 함수들인데요. 1.8.0 에서 stable 단계로 변경되었습니다.
val presentOptional = Optional.of("I'm here!")
println(presentOptional.getOrNull())
// "I'm here!"
val absentOptional = Optional.empty<String>()
println(absentOptional.getOrNull())
// null
println(absentOptional.getOrDefault("Nobody here!"))
// "Nobody here!"
println(absentOptional.getOrElse {
println("Optional was absent!")
"Default value!"
})
// "Optional was absent!"
// "Default value!"
kotlin-reflect 성능 개선
kotlin-reflect
가 이제 JVM 1.8 로 컴파일되면서 내부 캐시 메커니즘을 자바의 ClassValue
로 마이그레이션 할 수 있었습니다. 덕분에 기존에 캐싱되던 KClass
와 더불어 KType
과 KDeclarationContainer
또한 캐싱됩니다. 이 변화는 특히 typeOf()
를 호출할 때 큰 성능 향상을 가져왔습니다.
그 밖의 변화(Kotlin/Native, Kotlin/JS, Kotlin Multiplatform)
- Objective-C, Swift 와의 상호운용성 향상
- Xcode 14.1 지원
- 새로운 Android source set layout
- Kotlin/JS IR 컴파일러 백엔드 stable
- yarn.lock 파일 update 감지를 위한 새로운 세팅값(gradle properties)
- browser 테스트 타겟 지정 가능(gradle properties)
어느새 코틀린 버전이 1.8.0 까지 올라왔습니다.
릴리즈노트, 이와 관련된 youtrack 이슈들을 추적하면 정말 수 많은 개발자들이 코틀린에 이슈를 제기하고 이를 해결하는 과정을 볼 수 있는데, 이 과정을 같이 따라가면서 이해해보는 과정도 정말 재밌고 유익했습니다. 😌