/ KOTLIN

Kotlin 1.4 Online Event: Kotlin 1.4 Language Features 영상 정리

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 출시와 함께 온라인 이벤트가 진행되었습니다. 이 중, Kotlin 1.4 Language Features 세션에서 소개된 내용을 요약 정리해 보았습니다.

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

코틀린 1.4에서 추가된 주요 기능은 다음과 같습니다.

  • 코틀린 클래스의 SAM 변환 지원 추가
  • 명시적 API 모드
  • 후행 쉼표 지원
  • when 구문 내에서 breakcontinue 지원
  • 명명된 인수와 위치 인수 혼용 허용
  • 타입 추론 개선
  • 널 포인터 관련 에러 발생시 단일 타입의 예외 사용

코틀린 클래스의 SAM 변환 지원 추가

SAM 변환에서 SAM은 Single Abstract Method의 약자이며, 이렇게 추상 메서드 하나만으로 구성된 인터페이스를 SAM 인터페이스라 부릅니다. SAM 인터페이스의 예는 다음과 같습니다.

interface Action {
    fun run()
}

코틀린은 SAM 인터페이스를 사용해야 할 때 이를 람다식 (lambda expression)으로 간편하게 표기할 수 있게 도와주는데, 이를 SAM 변환 이라 부릅니다.

참고: Functional (SAM) interfaces - kotlinlang.org

아래와 같이 SAM 인터페이스인 Action 인터페이스와, 이를 인자로 받는 runAction() 메서드가 있다고 가정해 봅시다. (코드가 자바로 작성되어 있음을 기억해주세요)

public interface Action {
    void run();
}

public static void runAction(Action action) {
    action.run();
}

자바 코드에서 runAction() 코드를 호출하려면 아래와 같이 Action을 구현하는 추상 클래스를 생성하여 인자로 전달해야 합니다.

runAction(new Action() {
    @Override
    public void run() {
        System.out.println("Do someting");
    }
});

이를 코틀린 코드로 바꾸었을 때, SAM 변환을 사용하지 않는다면 자바 코드와 별반 다르지 않은(?) 복잡한 코드가 나옵니다.

runAction(object: Foo.Action {
    override fun run() {
        println("Do Something")
    }
})

하지만, 코틀린에서 제공하는 SAM 변환을 사용하면 앞의 코드를 다음과 같이 람다식을 사용하여 간결하게 표현할 수 있습니다.

runAction { println("Do something") }

보아하니 지금도 잘 지원되는 기능인 것 같은데, 코틀린 1.4에선 무엇이 달라졌을까요?

코틀린 1.4 이전까지는 SAM 변환이 자바로 작성된 인터페이스에서만 동작하고, 코틀린으로 작성된 인터페이스에서는 동작하지 않았습니다.

확인을 위해, 앞에서 자바로 작성했던 인터페이스를 코틀린으로 변환합니다.

interface Action {
    fun run()
}

fun runAction(a: Action) = a.run()

이전과 같이 runAction() 메서드를 호출하려 하면 컴파일 에러가 발생합니다.

// Error: Doesn't work. Please use function types.
runAction { println("Do something") }

이는 개발자라면 익숙할 버그가 아니라 기능, 즉 의도된 제약사항이였습니다. 😂

하지만, 많은 사람들의 요청 끝에 코틀린 1.4부터 코틀린 인터페이스도 SAM 변환을 지원하게 되었습니다.

단, SAM 변환을 지원할 인터페이스에 fun 키워드를 추가하여 인터페이스가 함수형 인터페이스 (funcional interface)임을 명시해야만 SAM 변환을 지원하며, 이 키워드가 없는 인터페이스에서는 여전히 SAM 변환을 지원하지 않습니다.

다음은 Action 인터페이스에 fun 키워드를 추가하여 SAM 변환을 지원하게끔 수정한 예를 보여줍니다.

fun interface Action {
    fun run()
}

...

// 성공: SAM 변환이 지원되므로 컴파일 에러가 발생하지 않습니다.
runAction { println("Do something") }

함수형 인터페이스로 선언된 인터페이스에 메서드를 하나 이상 추가하는 경우, 이는 더 이상 함수형 인터페이스가 아니므로 다음과 같이 컴파일 에러가 발생합니다.

// Error: fun interfaces must have exactly one abstract method
fun interface Action {
    fun run()
    fun runWithDelay()
}

명시적 API 모드

라이브러리를 유지보수 하다 보면, 작성자의 의도와 다르게 코드가 변경되는 경우가 종종 있습니다. 많은 사람들이 사용하는 라이브러리일수록, 이러한 실수가 발생하면 수많은 프로젝트에 영향을 줄 수 있습니다.

이는 대부분 간결함을 추구하는 코틀린의 특성 때문에 주로 발생하는데, 대표적인 예를 알아보겠습니다.

첫째, private로 선언해야 하는 함수를 실수로 public으로 선언할 수 있습니다. 코틀린은 접근 제한자가 없으면 이를 public으로 간주하기에, 의도치 않게 함수가 공개 API로 노출될 수 있습니다.

// 이 함수는 공개 API로 노출됩니다.
fun privateFun() { ... }

둘째, 함수 반환 타입이 의도지 않게 변경될 수 있습니다. 함수의 반환 타입이 명시되어 있지 않지만, 타입 추론을 통해 반환 타입이 String으로 확정됩니다.

fun getAnswer(finished: Boolean) = if (finished) "42" else "unknown"

코드를 다음과 같이 바꾸게 된다면, 경우에 따라 Int 또는 String을 반환하게 되므로 함수의 반환 타입은 Any로 바뀌게 됩니다.

fun getAnswer(finished: Boolean) = if (finished) 42 else "unknown"

명시적 (Explicit) API 모드를 사용하면, 앞의 사례와 같이 의도치 않게 공개 API의 스펙에 영향을 줄 수 있는 실수를 미연에 방지할 수 있습니다.

명시적 API 모드에서는 아래 두 가지 항목을 검사합니다.

  • 접근 제한자 존재 여부
  • 프로퍼티 및 함수의 타입 명시 여부

따라서, 앞의 두 예시는 다음과 같이 바꿔야만 명시적 API 모드를 통과할 수 있습니다.

// 공개 API는 명시적으로 public으로 선언해야 합니다.
public publicFun() { ... }

// 반환 타입을 명시해야 합니다.
fun getAnswer(finished: Boolean) : String = if (finished) "42" else "unknown"

명시적 API 모드를 사용하려면 빌드스크립트에 explicitApi 항목을 추가하면 됩니다. 아래는 명시적 API 모드를 활성화하고, 문제가 있으면 컴파일 에러 발생시킵니다.

  • build.gradle.kts
kotlin {
    explicitApi()
}
  • build.gradle
kotlin {
    explicitApi = 'strict'
}

컴파일 에러 대신 경고 수준으로 처리하고 싶다면 다음과 같이 빌드스크립트를 구성합니다.

  • build.gradle.kts
kotlin {
    explicitApiWarning()
}
  • build.gradle
kotlin {
    explicitApi = 'warning'
}

후행 쉼표 지원

코틀린 1.4부터 매개변수를 나열할 때 후행 쉼표 (trailing comma)를 사용할 수 있습니다.

지금까지는 매개변수를 나열할 때 매개변수 사이에만 쉼표를 추가할 수 있었습니다. 다음은 리스트를 생성하는 예를 보여줍니다.

val colors = listOf(
    "red", 
    "blue", 
    "green"
)

만약 마지막에 쉼표를 추가하면 다음과 같이 컴파일 에러가 발생했습니다.

// Error: Expecting ','
val colors = listOf(
    "red", 
    "blue", 
    "green",
)

코틀린 1.4 부터는 후행 쉼표를 지원하므로, 다음과 같이 다양한 곳에서 사용할 수 있습니다.

// 함수 호출
val colors = listOf(
    "red",
    "green",
    "blue",
)

// 함수 정의
fun displayRectangle(
    color: Color,
    width: Int,
    height: Int,
) {
    ...
}

// 클래스 정의
data class Contact(
    val address: String,
    val phoneNumber: String,
    val email: String,
)

when 구문 내에서 continuebreak 구문 지원

코틀린 1.3 까지는 반복문 내부의 when 구문 내에서 continuebreak 구문을 사용할 수 없었습니다.

fun foo(list: List<Int>) {
    for (i in list) {
        when (i) {
            // 에러: 'break' and 'continue' are not allowed in 'when' statements.
            // Consider using labels to continue/break from the outer loop.
            42 -> continue
            else -> println(i)
        }
    }
}

따라서, 대안으로 다음과 같이 라벨을 지정해서 사용해야 했습니다.

fun foo(list: List<Int>) {
    // 'label' 이라는 이름으로 라벨을 지정합니다.
    @label for (i in list) {
        when (i) {
            // 성공: 컴파일 에러가 발생하지 않습니다.
            42 -> continue@label
            else -> println(i)
        }
    }
}

코틀린 1.4 부터는 앞의 예처럼 라벨을 지정할 필요 없이 continuebreak 문을 사용할 수 있습니다. 따라서 코틀린 1.4에서는 다음 코드에서 더이상 컴파일 에러가 발생하지 않습니다.

fun foo(list: List<Int>) {
    for (i in list) {
        when (i) {
            // 성공: 코틀린 1.4부터는 라벨 없이 continue 및 break 문을 사용할 수 있습니다.
            42 -> continue
            else -> println(i)
        }
    }
}

명명된 인수와 위치 인수 혼용 허용

명명된 인수 (named arguments)를 사용하면 함수에 넘기는 인자를 보다 쉽게 구분할 수 있습니다. 다음은 명명된 인수를 사용하는 예를 보여줍니다.

fun drawRectangle(width: Int, height: Int, color: Color) { ... }

// 명명된 인수를 사용하여 drawRectangle() 함수를 호출합니다.
drawRectangle(width: 10, height: 20, color: Color.BLUE)

앞의 예에서도 볼 수 있듯이, widthheight는 명명된 인수를 사용함으로써 어떤 인자에 어느 값이 들어가는지 명확히 알 수 있게 되었습니다.

하지만, color는 넘겨주는 인자 자체만으로도 어떤 타입인지 충분히 알 수 있기 때문에, 오히려 코드만 길어질 뿐 큰 장점이 없습니다. 그렇다면, color 매개변수에만 명명된 인수를 사용하지 않을 순 없을까요?

코틀린 1.3 까지는 명명된 인수와 위치 인수 (positional argument)를 혼용할 수 없었습니다. 따라서 다음 코드는 컴파일 에러를 발생시킵니다.

// 오류: Mixing named and positional arguments is not allowed.
drawRectangle(width: 10, height: 20, Color.BLUE)

코틀린 1.4 부터는 명명된 인수와 위치 인수를 혼용해서 사용할 수 있습니다. 단, 위치 인수로 사용할 인자는 반드시 해당 인자의 순서에 맞게 사용해야 합니다. 다음은 혼용이 가능한 예와 그렇지 않은 예를 보여줍니다.

// 성공: 명명된 인자와 위치 인자를 혼용할 수 있습니다.
drawRectangle(width: 10, height: 20, Color.BLUE)

// 실패: Color.BLUE를 위치 인자로 넘겨주었으나, 올바른 위치 (세번째 인자)에 있지 않습니다.
drawRectangle(Color.BLUE, width: 10, height: 30)

타입 추론 개선

코틀린의 가장 큰 장점 중 하나인 타임 추론 (Type inference)이 더욱 개선되었습니다.

람다식 내 인자 타입 추론 지원

다음은 람다식을 값의 타입으로 사용하는 맵을 생성하는 코드입니다.

val rulesMap: Map<String, (String?) -> Boolean> =
    mapOf(
        "weak" to { it != null },
        "medium" to { !it.isNullOrBlank() },
        "strong" to { it != null && "^[a-zA-Z0-9]+$".toRegex().matches(it) }
    )

rulesMap의 타입을 Map<String, (String?) -> Boolean> 으로 지정했기 때문에, 람다식 내 인자인 itString? 타입이 되어야 합니다. 코틀린 1.3 까지는 위 경우 타입 추론이 동작하지 않아 컴파일 에러가 발생하였지만, 위 경우도 타입 추론을 지원하므로 컴파일 에러가 발생하지 않습니다.

람다의 반환 타입에 스마트 캐스트 지원

다음 코드에서 strnullable 타입이지만, 코틀린 컴파일러는 스마트 캐스트를 통해 non-null 타입과 동일하게 처리합니다.

val result = run {
    var str = currentValue()
    if (str == null) {
        str = "test"
    }
    str
}

하지만, 코틀린 1.3 까지는 람다의 반환 타입을 여전히 String?으로 처리하여 이를 받는 부분에서 별도로 널 처리를 해야 했습니다.

코틀린 1.4부터는 위 경우에 String? 대신 String을 반환하므로 보다 편리하게 반환값을 처리할 수 있습니다.

함수 참조 사용시 함수 인자의 기본값 지원

다음은 기본값을 가지고 있는 인자를 가진 함수의 예를 보여줍니다.

fun foo(i: Int = 0): String = "$i!"

(Int) -> String 을 인자로 받는 함수 apply가 있는 경우, foo 함수를 다음과 같이 함수 참조를 사용하여 넘겨줄 수 있습니다.

fun apply(func: (Int) -> String) : String = func()

// foo 함수를 함수 참조를 사용하여 인자로 넘겨줍니다.
apply(::foo)

코틀린 1.4부터는 foo 함수를 인자가 없는 타입, 즉 () -> String으로도 사용할 수 있습니다. 따라서 아래와 같이 apply() 함수의 인자 타입이 (Int) -> String 에서 () -> String으로 변경되어도 foo 함수를 대입할 수 있습니다.

fun foo(i: Int = 0): String = "$i!"

fun apply(func: () -> String) : String = func()

// 0! 을 반환합니다.
apply(::foo)

널 포인터 관련 에러 발생시 단일 타입의 예외 사용

기존에는 널(null) 관련 예외가 발생했을 때, 발생하는 유형에 따라 다음와 같이 다양한 종류의 예외가 발생했습니다.

  • KotlinNullPointerException
  • TypeCastException
  • IllegalStateException
  • IllegalArgumentException

코틀린 1.4 부터는 위 경우에 모두 NullPointerException 예외가 발생합니다. 따라서 에러 유형별로 에러 처리를 더욱 편리하게 할 수 있습니다.

다음은 User.name으로 인해 널 포인터 에러가 발생했을 때 예시를 보여줍니다. 에러 유형은 바뀌지만, 에러 메시지는 그대로 유지되는 것을 확인할 수 있습니다.

// Kotlin 1.3: IllegalStateException 발생
IllegalStateException: User.name must not be null

// Kotlin 1.4: NullPointerException 발생
NullPointerException: User.name must not be null

추가 리소스

앞에서 정리한 내용 외에 코틀린 1.4 업데이트와 관련된 자세한 내용은 개발자 문서를 참고하세요.

kunny

커니

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

Read More