[Kotlin] 1.8.0 업데이트 요약

allocProc
15 min readJan 2, 2023

--

코틀린 1.8.0 버전이 정식으로 출시되었습니다. 코틀린 깃허브에 릴리즈 노트가 공개되어있고 코틀린 공식 페이지에도 업데이트 소개 글이 있지만 영어로 작성되어 있기도 하고 설명이 다소 불친절한 부분도 있기에 그런 부분은 부가적인 설명을 채워가며 번역하고 정리했습니다.

큰 변화는 다음과 같습니다.

  • JVM 19 지원
  • 컴파일러 최적화 비활성화 옵션 제공
  • Lombok @Builder 어노테이션 지원
  • Gradle 7.2 & 7.3 지원
  • Kotlin Standard Library 개선
  • kotlin-reflect 성능 개선

Kotlin/JVM

Photo by Waldemar Brandt on Unsplash

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.gradlekotlin.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.TimeUnitkotlin.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 사이의 시간 차를 얻기위해 각각의 TimeMarkelapsedNow() 를 호출해서 그 차이를 계산했습니다. 하지만 이 결과는 다소 부정확하고 비결정적이었는데요. 여러 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 와 더불어 KTypeKDeclarationContainer 또한 캐싱됩니다. 이 변화는 특히 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 이슈들을 추적하면 정말 수 많은 개발자들이 코틀린에 이슈를 제기하고 이를 해결하는 과정을 볼 수 있는데, 이 과정을 같이 따라가면서 이해해보는 과정도 정말 재밌고 유익했습니다. 😌

--

--

No responses yet