반응형

기존에 자바에 대한 지식이 있는 상태에서 코틀린으로 넘어오신 분들은 코틀린에서 제공하는 대다수의 타입과 클래스들에 대해 거부감이 없으실테지만, 그럼에도 Any와 Unit까지는 어찌어찌 이해하겠는데 Nothing에 대해서는 헷갈려 하시는 분들이 많습니다. 이번 포스팅에서는 Nothing에 대해 정리하면서 헷갈리던 개념을 바로잡는 데에 도움을 드리고자 합니다.

 

본 글은 Kotlin Weekly #181에 기재된 "Any, Unit, Nothing and all their friends."를 의역하고 약간의 제 견해를 덧붙인 글입니다.

 

모든 객체의 조상 "Any"

먼저 Any부터 살펴보겠습니다.

Any는 간단합니다. Any는 "Root" 타입으로, 코틀린의 모든 타입은 Any를 상속합니다. 이는 Java에서 Object와 같은 개념이며, 실질적으로 바이트코드로 컴파일 됐을 때 자바의 Object와 일치합니다.

 

Kotlin

val greeting: Any = "Hello, World!"

 

Java

public final Object greeting = "Hello, World!"

 

위 코드에서 "Hello, World" 라는 값은 String 타입이지만, 앞서 말씀드린 것과 같이 Koltin의 모든 타입은 Any를, Java에서는 Object를 상속하고 있기 때문에 서브 클래스의 객체는 부모 클래스에 대입이 가능하다는 문법에 의해 String을 Any(Java에서는 Object) 타입에 대입할 수 있습니다.

 

코틀린에서 void는 "Unit"

코틀린의 Unit 타입은 Java의 void를 의미한다고 보면 됩니다.

함수의 리턴 타입을 Unit으로 지정하게된다면 Java에서의 void 리턴 타입을 지정했을 때처럼 실질적인 리턴 값이 없어야 하며, 명시적으로 return을 작성하지 않아도 됩니다.

 

Kotlin

fun returnsUnit(): Unit {
}

fun returnsUnitExplicitly1(): Unit {
    return
}

fun returnsUnitExplicitly2(): Unit {
    return Unit
}

 

Java

public final void returnsUnit() {
}

public final void returnsUnitExplicitly() {
    return;
}

 

위 코드에서 함수를 종료하는 표현 방식은 모두 다르지만 컴파일 결과는 동일합니다.

코틀린과 자바의 차이점이 있다면 코틀린에서는 return Unit 이라는 표현이 가능하다 정도되겠습니다.

 

코틀린은 왜 void가 아닌 Unit을 만들었을까?

코틀린의 Unit과 자바의 void가 아예 같다면, 굳이 같은 JVM 언어인 자바에서 잘 사용하고 있는 void를 이름을 바꿀 이유가 없어 보입니다.

 

코틀린의 Unit은 자바의 void와 다르게 두 가지 특징이 있습니다.

 

    1. Unit은 싱글톤 인스턴스입니다. 그래서 코틀린에서 Unit이라는 키워드는 타입이면서도 동시에 객체이기도 합니다.

val unit: Unit = Unit

 

    2. Unit은 객체이기도 하기 때문에, 코틀린의 모든 객체는 Any의 자식이다는 앞선 설명에 따라 Unit도 Any의 서브 클래스입니다.

val unit: Any = Unit

 

Unit과 헷갈릴 수 있는 "Nothing"

개인적으로 저는 코틀린 문법을 처음 접할 때에 가장 헷갈렸던 부분이 바로 이 Nothing 이었습니다.

 

Nothing은 어떠한 값도 포함하지 않는 타입입니다.

Nothing은 private constructor로 정의되어 있어 인스턴스를 생성할 수 없습니다.

 

코틀린 문서에는 다음과 같이 명시되어 있습니다.

Nothing has no instances. You can use Nothing to represent “a value that never exists”

 

생성자도 접근할 수 없고, 어떠한 값도 얻을 수 없다면 Nothing은 어떤 용도로 활용될 수 있을까요?

 

리턴 될 일이 없을 경우 or 예외를 던질 경우

1. 함수가 리턴 될 일이 없을 경우

여기서 Unit과의 차이가 있습니다.

Unit은 "아무런 값도 리턴을 하지 않는다"의 의미였습니다. 즉 리턴의 대상이 없을 뿐이지 리턴이라는 행위 자체를 하지 않는다는 의미는 아니었습니다.

반면에 Nothing은 리턴이라는 행위 자체를 하지 않음을 의미합니다.

 

예제를 살펴보겠습니다.

fun infiniteLoop(): Nothing {
    while (true) {
        println("Hi there!")
    }
}

위 코드는 무한루프를 돌고 있어서 infiniteLoop() 함수가 종료될 일이 없습니다.

코틀린 컴파일러는 똑똑해서 위 함수가 종료되지 않을 것이라는 것을 유추해냅니다. 그렇기 때문에 만약 리턴 가능한 함수에서 리턴 타입을 Nothing으로 정의할 경우 컴파일러가 컴파일 에러를 발생시켜줍니다.

즉, 함수가 리턴될 경우가 없기에 리턴 타입으로 Nothing을 쓰기에 적절한 상황입니다.

 

2. 예외를 던지는(throw Exception) 함수의 리턴 타입

Nothing은 예외를 던지는 함수에서의 리턴 타입으로 사용되기도 합니다.

 

이 경우는 예제부터 살펴보겠습니다.

fun throwException(): Nothing {
    throw IllegalStateException()
}

함수에서 예외를 던지는 것은 정상적으로 함수가 종료되는 것이 아니기 때문에 "함수가 리턴되었다"고 보지 않습니다. 따라서 1번(함수가 리턴될 일이 없는 경우)의 상황과도 맞아떨어집니다.

 

코틀린의 모든 타입은 기본적으로 non-null 입니다. 즉 타입이 null이 아님을 보장하고, 만약 null 값을 가질 수 있는 경우에는 타입 뒤에 물음표(?)를 붙여 Nullable임을 명시해주면됩니다.

 

그러면 함수의 리턴 타입이 Nothing? 일 경우에는 어떻게 될까요?

fun mayThrowAnException(throwException: Boolean): Nothing? {
    return if (throwException) {
        throw IllegalStateException()
    } else {
        println("Exception not thrown :)")
        null
    }
}

왠지 "리턴하지 않는다"와 "null을 리턴한다"는 공존할 수 없는 문법일 것 같지만, 위 예제와 같이

    1) 종료되지 않는 함수

    2) 예외를 던지는 함수

    3) null을 리턴하는 함수

이렇게 선택사항이 한 가지 늘어날 뿐입니다.

 

따라서 위 코드를 호출하게 된다면 return 될 수 있는 값은 반드시 null 입니다.(왜냐하면 null이 아니면 리턴되지 않기 때문이죠)

fun main() {
    val result = mayThrowAnException(true)
    if (result == null) { // Always true
        println("Ignored code")
    }
}

이 부분이 헷갈리실 수도 있지만 다행스럽게도 컴파일러는 result의 결과가 항상 null 이라는 것을 힌트로 알려줍니다.

 

Expression으로 살펴보는 Nothing

(부제: Nothing은 모든 타입의 서브타입이다.)

 

지금까지 함수의 리턴 타입으로의 Nothing을 살펴봤다면 이제는 조금 응용하여서 expression으로써의 Nothing을 살펴보겠습니다. expression이란 결과를 갖고 있는 연산식 정도로 이해하면 되겠습니다.

(일례로, 자바에서는 if문이 expression이 아니지만 코틀린에서는 expression입니다.)

 

val nullableValue: String? = null
val value = nullableString ?: throw IllegalStateException()

위 코드를 보시면 nullableValue는 String? 타입이라서 null 값이 들어올 수 있는 변수입니다.

그리고 value는 엘비스 연산자를 사용하여서 nullableString이 null이 아니라면 그 값을 대입하고, null일 경우에는 Exception을 던지도록 선언되어 있습니다. 이때 엘비스 연산자에 대응되는 nullableString은 String? 타입이고, throw IllegalStateException()은 Nothing 타입이라서 컴파일 에러를 띄워야할 것 같지만 위 코드는 정상적으로 컴파일이 가능합니다.

 

그 이유는 Nothing이 모든 타입의 서브 클래스이기 때문입니다.

그렇기에 위 코드의 value 변수에 대입되는 expression은 항상 String 타입으로 평가됩니다.

 

다른 예제를 보겠습니다.

val nullableValue: String? = null
val value: Int = nullableValue?.toInt() ?: return

이번에는 앞선 예제와 비슷한 코드이지만, throw 대신 return 이 사용되었습니다.

return 의 타입은 Nothing 입니다. 따라서 앞서 살펴본 throw를 사용했던 예제와 유사하다고 보시면 됩니다.

 

코틀린에서 Any는 모든 타입의 조상이라고 했습니다. 그리고 반대로, 코틀린에서 Nothing은 모든 타입의 자식이라고 했습니다.

그럼 Nothing은 내가 새로 만든 클래스의 자식이기도 할까요?

정답은 Yes. 기존에 코틀린에서 제공하고 있던 모든 클래스 뿐만 아니라 내가 만든 클래스도 포함하여 모든 클래스의 서브타입이라고 보시면 됩니다.

 

반응형
반응형