본문으로 바로가기
반응형

Photo by unsplash

Kotlin 버전이 릴리즈됨에 따라 적지 않은 변경점들과 신규 기능들이 추가 됩니다. 개인적으로는 v1.1부터 쓰기 시작했기 때문에 기본 문법은 그 버전에 맞춰져 있어, 버전 변경시마다 체크하지 않는다면, value class가 뭔지, K2 complier가 뭔지 모르는건 당연지사이고, 신규 기능들이 있는지 조차 모르고 시간이 지나갈 수 있습니다. 

최근에 릴리즈 되었던 세개의 major 버전에 대한 변경점을 위주로 (너무 오래된건 언급하기엔 좀 애매한듯 하여..) 정리를 진행하겠습니다. 

참고로 이 문서는 references에 링크된 kotlin 공식 문서를 기준으로 작성하였으며, 모든 내용을 담지는 않았음을 미리 알려드립니다. 

v1.6.0 changes

exhaustive when

when은 statement 나 expression 형태 모두 사용할수 있습니다.

  • statement: when문의 return 값을 사용하지 않음.
  • expression: when문의 return 값을 받아서 사용

when을 사용할때는 코드 안정성을 위하여 모든 요소를 나열하거나 else를 사용해야 합니다. expression인 경우 위 샘플과 같이 compile error를 나타내지만 statement 형태로 사용할 경우 모든 값을 나열하지 않아도 컴파일은 가능합니다. (warning으로 표시됨) 

v1.6.0 부터는 exhausetive 하지 않은 when을 사용할 경우 IDE에서 warning을 띄워 주도록 되어 있습니다. 하지만 v1.7.0 나온 현 시점에는 statement에도 누락된 element가 있다면 IDE 자체에서 error를 띄워 주므로 누락없는 when문을 작성할 수 있습니다.

Stable suspending functions as supertypes

suspending function을 전달받는 API를 사용할때 유용한 방법으로 필요한 동작을 class로 분리시키는게 가능해 집니다.

// suspend function을 상속 받는 class
class OnClickAction: suspend () -> Unit {
    override suspend fun invoke() {
        // do click action
    }
}

suspend fun onClickAction() {
    // do click action
}

fun onClicked(action: suspend() -> Unit) {
    runBlocking {
        action.invoke()
    }
}

fun test() {
    onClicked(::onClickAction)
    onClicked(OnClickAction())
    onClicked{ /* do click action*/ }
}

이 기능은 1.5.30에 소개되었으며 1.6.0부터 stable로 변경되었습니다. 다만 supertype으로 일반 함수 타입과 suspendingd 함수 타입을 동시에 쓸수 없으며, 여러개의 suspending functional supertypes를 사용하는것도 불가능 합니다.

단 아래와 같은 형식은 가능합니다.

class OnClickAction: suspend () -> Unit, suspend (Int) -> Unit {
    override suspend fun invoke() {
        // do click action
    }

    override suspend fun invoke(p1: Int) {
        // do click action
    }
}

Stable suspend conversion

1.4.0에서 소개 되었던 일반 함수를 suspending function으로 자동으로 converting 하는 작업이 stable로 바꼈습니다. 따라서 suspending type을 param으로 받는 함수에 일반함수를 넘겨 주더라도 compiler가 자동 변환하여 suspending으로 처리합니다.

suspend fun suspendFunction() {
    //Do something
}

fun normalFunction() {
    //Do something
}

fun onClickAction(suspending: suspend () -> Unit) {
    runBlocking {
        suspending()
    }
}

fun test() {
    onClickAction(::suspendFunction)
    onClickAction(::normalFunction)
    onClickAction{ /* do click action*/ }
}

(다만 suspending이 아닌 함수였다면 내부에서 suspend function을 사용하지 않았을거라고 예상되기에 함수 수행중에 suspend/resume이 수행되지는 않을것으로 예상됨.)

Builder inference

Generic값을 갖는 Builder의 경우 lambda 구문안에서 설정 type을 추론하여 Generic type을 결정합니다. 이는 "-Xenable-builder-infernece" 옵션을 켜야만 동작합니다만, v1.7.0 버전에서는 이 추가 compile option을 명시하지 않아도 기본으로 적용되어 동작하며 stable로 (v1.7.0에서) 변경되었습니다.

list를 만드는 buildList를 예로 들면 IDE에 넘어온 receiver가 MutableList<out Any?>로 hint를 주고 있습니다. 게다가 Generic type이 명시되지 않았기 때문에 아직은 compile error 상태로 나타 납니다.

만약 기존 버전이었다면 아래와 같이 만들어질 MutableList의 type을 지정해야 합니다. 

IDE에서 Lambda 영역의 receiver가  MutableList<String>으로 되어 있음을 알려 줍니다. 하지만 builder 추론을 이용하면 lambda 영역에서 사용된 값을 이용하여 generic type이 추론됩니다.

Generic type을 명시하지는 않았지만 resultList1add()로 string을 넣었기 때문에 최종 generic type이 String이 되며, resultList2의 경우 get() 함수의 retrun값을 Int로 명시했기때문에 generic type이 int로 처리 됩니다.[4]

v1.6.20 Changes

Context receiver 

함수나, properties, class를 context dependent하게 만들고 한개 이상의 context receiver를 갖도록 정의할 수 있습니다. (단 "-Xcontext-receivers" 옵션을 enable 해야 합니다.)

receiver를 사용할 부분에서는 context()를 이용하여 사용할 context receiver를 지정하고, 이를 호출하는(사용하는) caller 쪽에서 지정된 receiver를 넘겨줘야 합니다.

// 공식 kotlin 페이지 코드 입니다.
// https://kotlinlang.org/docs/whatsnew1620.html#prototype-of-context-receivers-for-kotlin-jvm

interface LoggingContext {
    val log: Logger // This context provides a reference to a logger
}

context(LoggingContext)
fun startBusinessOperation() {
    // LoggingContext를 암묵적인 receiver로 지정했기 때문에 바로 호출 가능합니다.
    log.info("Operation has started")
}

fun test(loggingContext: LoggingContext) {
    with(loggingContext) {
        // 하기 함수를 호출하기 위해서는 호출부분에서 해당 receiver 영역을 만들어
        // 이 안에서 호출해야 합니다.
        startBusinessOperation()
    }
}

이 기능은 prototype임을 강조하고 있고 toy project에서 사용하라는 권고도 되어 있으니, 아직 사용은 좀 기다려 봐야 할것 같습니다.

Non-nullable Generic type의 추가

Generic type을 선언할때 NonNull 타입을 명시할 수 있는 T&Any라는 새로운 문법이 추가됩니다.  (아직 베타 버전이지만 거의 안정화 된 상태라고 하네요)

반응형
// 공식 kotlin 페이지 코드 입니다.
// https://kotlinlang.org/docs/whatsnew1620.html#prototype-of-context-receivers-for-kotlin-jvm

fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y

fun main() {
    // OK
    elvisLike<String>("", "").length
    // Error: 'null' cannot be a value of a non-null type
    elvisLike<String>("", null).length

    // OK
    elvisLike<String?>(null, "").length
    // Error: 'null' cannot be a value of a non-null type
    elvisLike<String?>(null, null).length
}

(이를 위해서는 아래와 같이 language version값이 1.7 이상이 선언 되어야 합니다.)

kotlin {
    sourceSets.all {
        languageSettings.apply {
            languageVersion = "1.7"
        }
    }
}

Default method를 위한 @JvmDefaultWithCompatibility annotation 추가

해당 annotation을 추가하여 kotlin에 정의된 non-abstract method를 JVM interface의 default method로 compile 할 수 있습니다. 또한 이를 위해서는 "-Xjvm-default=all" 옵션을 추가해야 합니다. [5]

예를 들어 kotlin으로 아래와 같은 interface를 만들었다고 합니다.

// 공식 kotlin 페이지 코드 입니다.
// https://kotlinlang.org/docs/java-to-kotlin-interop.html#default-methods-in-interfaces


// compile with -Xjvm-default=all
interface Robot {
    fun move() { println("~walking~") }  // will be default in the Java interface
    fun speak(): Unit
}

이렇게 만들어진 default method를 포함하는 interface는 아래와 같이 java code에서 사용할 수 있습니다.

//Java implementation
public class C3PO implements Robot {
    // move() 함수는 Robot interface에서 구현된 default method가 호출됩니다.
    @Override
    public void speak() {
        System.out.println("I beg your pardon, sir");
    }
}
C3PO c3po = new C3PO();
c3po.move(); // Robot interface에 정의된 default method가 호출됩니다.
c3po.speak();

또한 이 코드는 java의 기본 default method 처럼 override도 가능합니다.

//Java
public class BB8 implements Robot {
    //own implementation of the default method
    @Override
    public void move() {
        System.out.println("~rolling~");
    }

    @Override
    public void speak() {
        System.out.println("Beep-beep");
    }
}

만약 이 옵션 없이 사용할 경우 컴파일된 코드는 binary상으로 호환되지 않을수 있습니다.

1.6.20 이전에는 반대의 방식으로 "-Xjvm-default=all-compatibility" 옵션을 사용하고, 새로운 interface를 정의할때 호환성이 필요하지 않은 경우에 대해서 @JvmDefaultWithoutCompatibility를 붙였습니다.

변경된 사유에 대한 내용은 이 정도로 각설하고, 정리하면, 1.6.20부터는 "-Xjvm-default=all" 옵션을 사용하고, defatul method의 호환성이 필요한 inteface에는 @JvmDefaultWithCompatibility를 붙여 주면 됩니다.

JVM backend 에서 single module에 대한 parallel compile 지원

기존에 gradle에서 제공하는 parallel build는 여러개의 모듈을 동시에 빌드함으로써 빌드 시간을 높이는데 주안점을 두었습니다. 따라서 프로젝트가 여러개의 모듈로 잘 나누어져 있을경우 이 option을 켜면 빌드 속도를 향상시킬수 있습니다. 다만 모듈이 한개인 경우나, 한개의 모듈에 대부분의 코드가 집중된 경우 (아마도 이런 프로젝트가 대부분이지 않을까 싶네요) 효력을 발휘하지 못합니다. 

v1.6.20부터는 single moudule에 대해서 parallell compile 하기 위한 방법으로 experimental 기능인 "-Xbackend-threads" 옵션을 제공하며 이는 하나의 큰 모듈로 구성되어 있는 프로젝트에서 효율적으로 compile 시간을 줄일 수 있습니다. 

History로 봤을때 v1.5.20에서 새로운 JVM IR backend가 적용되었고, v1.6.20에서 parallel compile 적용으로 빌드 속도가 15%정도 향상 되었다고 합니다.[6][7][8] 참고로 아래서 추가로 설명할 v1.7.0은 5~10%더 빨라졌다고 합니다.

- compile option에 쓰이는 추가 argument

  • N: compile에 사용할 thread 개수. 단! cpu core 개수 이상으로 thread context switching으로 인하여 비효율적으로 동작 할수 있으므로 core 개수 이하로 사용할것
  • 0: 각 CPU core 개수만큼 thread를 사용함.

실제로 이를 사용하려면 project의 build.gradle에 아래와 같이 옵션을 추가 하면 됩니다.

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    kotlinOptions {
        freeCompilerArgs= listOf("-Xbackend-threads=0")
    }
}

향상된 수치는(15%??) 어디까지나 자체 측정한 수치입니다. 물론 공식 홈페이지에서 언급했으니 어느정도 신빙성은 있습니다. 다만 적용전에 본인의 프로젝트에서 빌드속도 측정을 통하여 정말 효과가 있는지 확인후에 적용하는것도 좋을것 같습니다. (실제로 제가 개발하는 프로젝트에서는 적용 전과 후가 오차범위안에서 움직였기에 효과가 없다고 판단했습니다.)

v1.7.0 Changes

JVM에서 동작하는 Kotlin용 K2 compiler alpha의 release

v1.7.0에서 가장 큰 변경점은 새로운 compiler가 적용되었다는점 입니다. (물론 아직 alpha 버전이라 사용하려면 좀더 기다려야 할것 같습니다만)

성능 향상에 주안점을 두었으며, JVM에서만 유효합니다. (Kotlin/JS, Kotlin/Native or other multi-platform projects에서는 지원 되지 않으며 kapt 같은 complier plugin 역시 지원하지 않음)

초당 처리 속도가 2.2K line에서 4.8K line으로 2.2배 빨라졌으며 K2 compiler를 적용하려면 "-Xuse-k2"를 complie option으로 추가 하면 됩니다. 

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    kotlinOptions {
        freeCompilerArgs= listOf("-Xuse-k2")
    }
}

다음 kotlin 버전에서 좀더 안정되고 확장된 기능을 제공하는 버전이 제공될 예정이라고 합니다.

value class의 inlined value를 delegation으로 사용 가능해짐

lightweight한 wrapper를 만들기 위해 구현하는 value classs (v1.5 이전에는 inline class)의 inlined 값을 delegation으로 사용이 가능해 집니다.

사실 이 부분은 value class와 by를 이용한 delegation을 얼마나 자주 쓰느냐에 따라서 효용성이 달라질것 같은데, 아래와 같은 코드가 v1.7.0 부터는 가능하다고 합니다. (v1.7.0 이전에는 안됐었던것 같은데, 자주 활용을 안하는 코드들이라 몰랐네요.;;)

interface Checker {
    fun check() = "OK!"
}

@JvmInline
value class ItemId(val id: Long)

@JvmInline
value class LocationId(val id: Long)

@JvmInline
value class Title(val title: String)

@JvmInline
value class Description(val description: String)

@JvmInline
value class CheckerWrapper(val checker: Checker) : Checker by checker


fun insertInfo(id: ItemId,
               locationId: LocationId,
               title: Title,
               description: Description) {
    val checker = CheckerWrapper(object : Checker {})
    println("{${id.id}, ${locationId.id}, ${title.title}, ${description.description}")
}

즉 위 코드에서 CheckerWrapper의 value인 "check" 값이 (컴파일시 inline으로 처리되는) by를 이용하여 Bar interface를 delegation 기법을 이용하여 대체 될수 있습니다.

Underscore operator '_' 를 이용한 타입추론

다른곳에서 type이 확정되는 Generic의 type은 '_' 를 쓰더라도 type 추론을 통하여 컴파일러가 알아서 처리 합니다.

abstract class SomeClass<T> {
    abstract fun execute(): T
}

class SomeImplementation : SomeClass<String>() {
    override fun execute(): String = "Test"
}

class OtherImplementation : SomeClass<Int>() {
    override fun execute(): Int = 42
}

object Runner {
    inline fun <reified S: SomeClass<T>, T> run(): T {
        return S::class.java.getDeclaredConstructor().newInstance().execute()
    }
}

fun main() {
    // T is inferred as String because SomeImplementation derives from SomeClass<String>
    val s = Runner.run<SomeImplementation, _>()
    assert(s == "Test")

    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>
    val n = Runner.run<OtherImplementation, _>()
    assert(n == 42)
}

위 코드에서 SomImplementation 이나 OtherImplementation의 type은 String, Int로 각각 지정되어 있습니다. 따라서 같은 타입을 사용하는 다른 generic type은 동일한 값을 명시하지 않고 '_'로 표현해도 complier가 타입추론으로 자동 결정합니다.

Builder inferneces change to stable

v1.6.0에서 소개된 타입추론의 특수 케이스중의 하나인 Generic 값을 갖는 Builder 추론 기능이 안정화 되어 자동으로 처리 됩니다. 따라서 builder inference를 사용하기 위해 추가했던 "-Xenable-builder-inference" 옵션은 더이상 필요하지 않습니다.[4]

자세한 내용은 위 v1.6.0의 내용에서 소개했으므로 여기서는 생략합니다.

Definitely non-nullable types chnage to stable

v1.6.20에서 추가된 nonnull generic type인 T & Any 가 stable로 변경되었습니다.

Compiler performance optimizations

Kotlin/JVM compiler에서 compile 속도가 평균 10% 정도 감소했다고 합니다.

New Compiler option: -Xjdk-release

이 옵션은 javac 명령어에 --release 옵션과 유사합니다. 여기서 설정된 값으로 bytecode의 버전과 사용할수 있는 JDK의 API를 제한할 수 있습니다. ex) -Xjdk-release=1.8

Callable references to functional interface constructors

functional interface의 생성자에 대한 callable refereneces 기능이 stable로 바꼈습니다. callable references에 대한 사용 방법은 공식 문서를 참조하시면 됩니다.[9]

Revmoed JVM target version 1.6

JVM의 기본 target version이 1.8로 변경되었습니다. 아마도 빌드시 java 8의 장점 (lambda의 compile 처리)이 적용되면 좀더 효율적인 bytecode가 생성될듯 싶네요.

References

[1] https://kotlinlang.org/docs/whatsnew16.html

[2] https://kotlinlang.org/docs/whatsnew1620.html

[3] https://kotlinlang.org/docs/whatsnew17.html

[4] https://kotlinlang.org/docs/using-builders-with-builder-inference.html

[5] https://kotlinlang.org/docs/java-to-kotlin-interop.html#default-methods-in-interfaces

[6] https://youtrack.jetbrains.com/issue/KT-46085/Support-experimental-parallel-compilation-of-a-single-module-in-the-JVM-backend

[7] https://youtrack.jetbrains.com/issue/KT-48233/Switching-to-JVM-IR-backend-increases-compilation-time-by-more-than-15

[8] https://youtrack.jetbrains.com/issue/KT-46768

[9] https://kotlinlang.org/docs/reflection.html#callable-references

반응형