/ KOTLIN

Kotlin 1.4 Online Event: News From the Kotlin Standard Library 영상 정리

Kotlin 1.4 Online Event 영상 정리 시리즈

  1. Kotlin 1.4 Language Features
  2. News From the Kotlin Standard Library (현재 글)
  3. kotlinx.serialization 1.0

코틀린 1.4 출시와 함께 진행된 온라인 이벤트News From the Kotlin Standard Library 세션에서 소개된 내용을 요약 정리해 보았습니다.

전체 세션 내용은 아래 영상에서 확인하실 수 있습니다.

이 세션에서 다루는 내용은 크게 다음과 같습니다.

  1. New functionality in Kotlin 1.4
    • 코틀린 1.4에서 표준 라이브러리에 새로 추가된 기능 소개
  2. Using stdlib in multiplatform projects
    • 멀티플랫폼 프로젝트를 위한 표준 라이브러리 변경사항 소개
  3. 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
    • 네이티브: 각 네이티브 타겟별 표준 라이브러리

기존에는 개별 소스 셋 (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()) // 시작 시각부터 현재까지 경과한 시간을 반환합니다.

추가 리소스

kunny

커니

안드로이드와 오픈소스, 코틀린(Kotlin)에 관심이 많습니다. 한국 GDG 안드로이드 운영자 및 GDE 안드로이드로 활동했으며, 현재 구글에서 애드몹 기술 지원을 담당하고 있습니다.

Read More