Kotlin 1.4 Online Event: News From the Kotlin Standard Library 영상 정리
Kotlin 1.4 Online Event 영상 정리 시리즈
코틀린 1.4 출시와 함께 진행된 온라인 이벤트 중 News From the Kotlin Standard Library 세션에서 소개된 내용을 요약 정리해 보았습니다.
전체 세션 내용은 아래 영상에서 확인하실 수 있습니다.
이 세션에서 다루는 내용은 크게 다음과 같습니다.
- New functionality in Kotlin 1.4
- 코틀린 1.4에서 표준 라이브러리에 새로 추가된 기능 소개
- Using
stdlib
in multiplatform projects- 멀티플랫폼 프로젝트를 위한 표준 라이브러리 변경사항 소개
- Experimental functionality
- 표준 라이브러리에 새로 추가된 실험적인 기능 소개
New functionality in Kotlin 1.4
코틀린 1.4 버전의 표준 라이브러리에는 다음 항목들이 반영되었습니다.
- 표준 라이브러리의 일관성 개선
- 새 함수 추가 -
runningFold()
,runningReduce()
- 새 타입 추가 -
ArrayDeque
- 비트 조작 연산자 추가
각 항목별로 자세한 내용을 살펴보겠습니다.
표준 라이브러리의 일관성 개선
코틀린은 널(null
)에 관대하지 않는 언어입니다. 때문에, 코틀린에서 제공하는 함수는 이름만 봐도 널 값을 반환하는지 아닌지 쉽게 확인할 수 있습니다. 다음은 몇몇 예를 보여줍니다.
함수 | 동작 |
---|---|
String.toInt() |
문자열을 정수로 변환합니다. 문자열이 숫자가 아닌 경우 NumberFormatException 예외가 발생합니다. |
String.toIntOrNull() |
문자열을 정수로 변환합니다. 문자열이 숫자가 아닌 경우 null 을 반환합니다. |
kotlin.collections.first() |
컬렉션 내 첫번째 인자를 반환하며, 컬렉션이 비어 있다면 NoSuchElementException 예외가 발생합니다. |
kotlin.collections.firstOrNull() |
컬렉션 내 첫번째 인자를 반환하며, 컬렉션이 비어 있다면 null 을 반환합니다. |
앞의 예에서 볼 수 있듯이, 코틀린 표준 라이브러리의 함수들은 대게 null
을 반환하는 버전과 그렇지 않은 버전을 모두 지원합니다.
하지만, 컬렉션 내 임의의 항목을 반환하는 kotlin.collections.random()
함수는 null
을 반환하는 버전이 누락되어 있었는데요, 코틀린 1.4 업데이트에 kotlin.collections.randomOrNull()
이 추가되면서 다른 함수와 마찬가지로 일관성을 갖추게 되었습니다.
다음으로 주목해볼 함수는 kotlin.collections.max()
와 kotlin.collections.min()
함수입니다. 코틀린 함수의 명명 규칙을 따른다면 각 함수는 컬렉션의 최대/최솟값을 반환하거나, 컬렉션이 빈 경우 예외를 일으켜야 합니다.
놀랍게도, 이들 함수는 컬렉션이 빈 경우 예외를 일으키는 대신 null
값을 반환합니다. 즉, 명명 규칙과 어긋나게끔 동작한다고 볼 수 있습니다.
이에도 나름의 이유가 있습니다. 이 함수들은 코틀린이 최초로 공개된 시점, 즉 1.0 버전부터 있던 함수이며, 그 당시에는 지금처럼 명명 규칙이 잘 갖춰져 있지 않았습니다. 어떻게 보면 당시에는 맞고, 지금은 틀리다(?)의 희생양이 된 셈이죠.
여하튼, 이를 개선하기 위해 kotlin.collections.max()
, kotlin.collections.min()
함수를 폐기 (deprecated) 처리하고, kotlin.collections.maxOrNull()
과 kotlin.collections.minOrNull()
함수를 추가했습니다. 이름에서 알 수 있듯이 이들 함수의 동작은 기존의 max()
및 min()
함수와 동일합니다.
max()
및 min()
함수에서 파생된 함수인 maxBy()
, minBy()
또한 다음과 같이 동일한 방식으로 변경되었습니다.
기존 함수 (폐기됨) | 새로 추가된 함수 (대체됨) |
---|---|
kotlin.collctions.maxBy() |
kotlin.collections.maxByOrNull() |
kotlin.collections.minBy() |
kotlin.collections.minByOrNull() |
이 외에, 컬렉션 내 항목의 최대/최솟값 ‘자체’를 반환하는 함수가 추가되었습니다.
함수 | 동작 |
---|---|
kotlin.collections.maxOf() |
컬렉션 내 가장 큰 값의 ‘값 자체’를 반환합니다. 컬렉션이 비어 있다면 NoSuchElementException 을 일으킵니다. |
kotlin.collections.minOf() |
컬렉션 내 가장 작은 값의 ‘값 자체’를 반환합니다. 컬렉션이 비어 있다면 NoSuchElementException 을 일으킵니다. |
kotlin.collections.maxOfOrNull() |
컬렉션 내 가장 큰 값의 ‘값 자체’를 반환합니다. 컬렉션이 비어 있다면 null 을 반환합니다. |
kotlin.collections.minOfOrNull() |
컬렉션 내 가장 작은 값의 ‘값 자체’를 반환합니다. 컬렉션이 비어 있다면 null 을 반환합니다. |
다음은 maxByOrNull()
과 maxOf()
함수가 어떻게 다르게 동작하는지 보여줍니다.
// maxByOrNull() 함수는 컬렉션 내 원소 (사람)를 반환합니다.
val oldest = people.maxByOrNull { it.age }
val maxAge = oldest?.age
// maxOf() 함수는 컬렉션 내 최댓값 자체 (나이)를 반환합니다.
val maxAge = people.maxOf { it.age }
표준 라이브러리에서 제공하는 함수 중 forEach
, filter
, map
과 같은 함수는 컬렉션 내 원소를 순회하면서 특정 작업을 수행할 때 사용합니다. 이 중, *Indexed
가 이름에 포함된 함수는, 컬렉션 내 현재 처리하는 원소의 인텍스를 인자로 받을 있습니다. 다음은 몇몇 예를 보여줍니다.
함수 | 인덱스를 제공하는 버전 |
---|---|
kotlin.collections.forEach() |
kotlin.collections.forEachIndexed() |
kotlin.collections.filter() |
kotlin.collections.filterIndexed() |
kotlin.collections.map() |
kotlin.collections.mapIndexed() |
이러한 함수 중 kotlin.collections.onEach()
와 kotlin.collections.flatMap()
함수는 인덱스가 제공되는 버전의 함수가 누락되어 있었는데, 코틀린 1.4에서 각각 kotlin.collections.onEachIndexed()
, kotlin.collections.flatMapIndexed()
함수가 추가되었습니다.
그 외에, kotlin.collections.reduce()
및 kotlin.collections.reduceIndexed()
의 null
을 반환하는 버전인 kotlin.collections.reduceOrNull()
, kotlin.collections.reduceOrNullIndexed()
함수도 추가되었습니다.
다음은 reduceOrNull()
및 reduceOrNullIndexed()
함수의 사용 예를 보여줍니다.
val list = listOf(1,2,3)
println(list.reduceOrNull { a, b, -> a + b}) // 6을 출력합니다.
val emptyList = emptyList<Int>()
println(emptyList.reduceOrNull { a, b -> a + b}) // null을 출력합니다.
emptyList.reduceIndexedOrNull {index, acc, e -> ... } // index를 사용하여 현재 순회중인 원소의 인덱스를 알 수 있습니다.
새 함수 추가 - runningFold(), runningReduce()
kotlin.collections.reduce()
와 kotlin.collections.fold()
함수는 컬렉션 내 모든 원소의 값을 합쳐 하나의 값으로 만들어줍니다. 두 함수의 전반적인 기능은 똑같고, fold()
험수는 reduce()
함수와 달리 초기값을 지정할 수 있다는 부분만 살짝 다릅니다.
코틀린 1.4에는 kotlin.collections.runningReduce()
와 kotlin.collections.runningFold()
함수가 추가되었습니다. reduce()
와 fold()
함수가 최종 결과값만 반환하는 것과 달리, runningReduce()
및 runningFold()
는 계산 중간 과정을 모두 반환합니다.
다음은 각 함수별 동착 차이를 보여줍니다.
// Result: 15 를 출력합니다.
print("Result: ${(1..5).reduce { sum, elem -> sum + elem }}")
// Result: [1, 3, 6, 10, 15] 를 출력합니다.
println("Result: ${(1..5).runningReduce { sum, elem -> sum + elem}}")
// Result: 15 를 출력합니다.
println("Result: ${(1..5).fold(0) { sum, elem -> sum + elem}}")
// Result: [1, 3, 6, 10, 15] 를 출력합니다.
println("Result: ${(1..5).runningFold(0) { sum, elem -> sum + elem}}")
새 타입 추가 - ArrayDeque
ArrayDeque
는 양 끝에서 삽입/삭제가 가능한 자료구조로, 코틀린 1.4부터 표준 라이브러리에 추가되었습니다.
이는 코틀린 공통 라이브러리 (common library)에 포함되어 있으므로, 특정 타겟 (예: JVM)을 대상하는 프로젝트가 아닌 다양한 플랫폼을 대상으로 하는 멀티플랫폼 프로젝트에서도 자유롭게 사용할 수 있습니다.
이 타입과 관련도니 자세한 내용은 ArrayDeque
개발자 문서를 참조하세요.
비트 조작 연산자 추가
비트 형태로 표현된 데이터를 더욱 편리하게 처리할 수 있는 함수가 추가되었습니다.
함수 | 설명 |
---|---|
kotlin.countOneBits() |
이진수 표현에서, 1인 비트의 총 개수를 반환합니다 |
kotlin.countLeadingZeroBits() |
이진수 표현에서, 최상위 비트로부터 연속으로 0인 비트의 수를 반환합니다. |
kotlin.countTrailingZeroBits() |
이진수 표현에서, 최하위 비트로부터 연속으로 0인 비트의 수를 반환합니다. |
kotlin.takeHighestOneBit() |
값이 1인 최상위 비트로만 구성된 숫자를 반환합니다. |
kotlin.takeLowestOneBit() |
값이 1인 최하위 비트로만 구성된 숫자를 반환합니다. |
다음은 각 함수의 사용 예를 보여줍니다.
val number = "1010000".toInt(radix = 2)
println(number.countOneBits()) // 2 출력
println(number.countLeadingZeroBits()) // 25 출력 (참고: number의 데이터 타입은 Int(32bits) 입니다)
println(number.countTrailingZeroBits()) // 4 출력
println(number.takeHighestOneBit().toString(radix = 2)) // 1000000 출력
println(number.takeLowestOneBit().toString(radix = 2)) // 10000 출력
Using stdlib in multiplatform projects
코틀린 멀티플랫폼 프로젝트를 개발하는 경우, 모든 플랫폼에서 사용하는 공용 라이브러리와 각 플랫폼에 특화된 라이브러리를 나누어 개발합니다.
이 때, 각 플랫폼 별로 참조하는 코틀린 라이브러리 또한 각각 나뉘게 됩니다. 다음은 플랫폼/모듈별 참조하는 코틀린 라이브러리의 예를 보여줍니다.
- 공용 라이브러리:
stdlib-common
- 타겟 플랫폼
- JVM:
stdlib-jvm
- Javascript:
stdlib-js
- 네이티브: 각 네이티브 타겟별 표준 라이브러리
- JVM:
기존에는 개별 소스 셋 (source set)별로 코틀린 표준 라이브러리 및 버전을 별도로 지정해야 했습니다.
하지만, 코틀린 1.4부터는 이를 별도로 지정하지 않아도 코틀린 그래들 플러그인 버전과 동일한 버전의 표준 라이브러리를 자동으로 의존성에 추가합니다.
안드로이드 프로젝트의 경우, 프로젝트 최상위 폴더 내의 build.gradle
에 코틀린 그래들 플러그인이 적용되어 있다면 앱 프로젝트 내 빌드스크립트 (app/build.gradle
)의 dependencies
섹션에 코틀린 표준 라이브러리를 추가하지 않아도 됩니다.
그 외에, JVM 타겟에서만 사용할 수 있었던 StringBuilder
또한 모든 플랫폼에서 사용할 수 있게끔 공통 라이브러리에 추가되었으며, Throwable.printStackTrace()
와 같은 예외 처리를 위한 API도 함께 추가되어 플랫폼과 무관하게 예외 처리를 통합적으로 할 수 있게 되었습니다.
Experimental functionality
API를 개발하다 보면 간혹 알파 버전의 API를 테스트 목적으로 배포해야 할 때가 있습니다. 이러한 API들은 추후 버전에서 제거되거나 변경될 수 있기에, 이를 사용하는 개발자들은 이를 꼭 염두하고 있어야 합니다.
코틀린 1.4에서는 이러한 용도로 사용할 수 있도록 @RequiresOptIn
어노테이션이 추가되었습니다. 이 어노테이션을 사용하면 개발자가 알파 버전의 API를 사용하는 것에 동의한다는 것을 명시하도록 강제할 수 있습니다.
다음은 @RequiresOptIn
어노테이션을 사용하여 사용자 정의 어노테이션을 정의하는 예시를 보여줍니다. 경로 수준 (Level.WARNING
, Level.ERROR
) 및 표시할 메시지를 원하는 대로 설정할 수 있습니다.
@RequiresOptIn(
level = Level.WARNING,
message = "This API can change"
)
annotation class BleedingEdgeAPI
앞에서 생성한 어노테이션은 다음과 같이 클래스 및 함수에 적용할 수 있습니다.
@BleedingEdgeAPI
class Foo {
...
}
@BleedingEdgeAPI
fun fetchFoo(): Foo { ... }
앞의 예시에 나온 Foo
클래스를 아무런 처리 없이 사용하는 경우, 다음과 같이 컴파일 에러가 발생합니다.
fun doSomething() {
// 오류: This API can change 메시지와 함께 컴파일 에러가 발생합니다.
val foo = Foo()
}
이를 해결하려면, @OptIn
어노테이션을 사용하여 명시적으로 ‘안정되지 않은 버전의 API를 사용하는데 동의한다’ 라 선언해 주어야 합니다. 다음과 같이 BleedingEdgeAPI
사용을 명시적으로 선언해주면 컴파일 에러를 해결할 수 있습니다.
@OptIn(BleedingEdgeAPI::class)
fun doSomething() {
// 성공: BleedingEdgeAPI를 사용함을 명시적으로 선언했으므로 컴파일 에러가 발생하지 않습니다.
val foo = Foo()
}
이 외에, 컬렉션을 만들 때 사용할 수 있는 실험적인 API 몇 개가 추가되었습니다.
kotlin.collections.buildList()
kotlin.collections.buildSet()
kotlin.collections.buildMap()
다음은 buildList()
의 사용 예를 보여줍니다.
val needsZero = true
val initial = listOf(2, 6, 41)
val ints = buildList {
if (needsZero) {
add(0)
}
initial.mapTo(this) { it + 1 }
}
println(ints) // [0, 3, 7, 42] 출력
추가로, 시간 측정에 사용할 수 있는 kotlin.time.measureTime()
함수도 추가되었습니다. 이 함수는 인자로 받는 블록을 실행하고, 실행 완료까지 걸린 시간을 반환합니다.
다음은 measureTime()
의 사용 예를 보여줍니다. 소요 시간은 Duration
타입으로 반환되므로, 필요에 따라 원하는 단위로 쉽게 변환할 수 있습니다.
import kotlin.time.*;
val duration: Duration = measureTime {
Thread.sleep(1050)
}
duration.inSeconds // 1.056486657
duration.inMilliseconds // 1056.486657
TimeSource.markNow()
/ TimeSource.elapsedTime()
을 사용하면 원하는 곳에서 자유롭게 시간을 측정할 수 있습니다. 다음은 사용 예를 보여줍니다.
import kotlin.time.*;
val clock = TimeSource.Monotonic
val mark = clock.markNow() // 시작 시각을 기록합니다.
Thread.sleep(200)
println(mark.elapsedNow()) // 시작 시각부터 현재까지 경과한 시간을 반환합니다.