언어(Language)/Kotlin

[Kotlin] 3. 코틀린의 NULL 처리 방법 (Nullable과 Non-Null, Safe Call, Null Safety, 엘비스 연산자 등)

잇트루 2022. 8. 17. 10:33
반응형

널(NULL)

널(NULL)이란 아무것도 없는 것을 뜻하는 단어입니다. 따라서 0조차 아니라는 것으로 프로그래밍 언어에서의 null은 문자열의 끝을 나타내는 특수 문자로 쓰이기도 하며, 존재하지 않는 메모리 주소로 나타내기도 합니다. 특히 Java에서는 사용할 수 없는 null인 변수에 접근하면서 생기는 오류인 NPE(NullPointerException)가 자주 발생하기도 하여 많은 개발자를 괴롭히기도 합니다.

 

코틀린의 null

코틀린의 기본 변수 선언은 null을 허용하지 않습니다.

val a: Int = 30
// a의 값에 30이라는 정수 값을 할당

var b: String = "Hello"
// b의 값에 Hello라는 문자열 값을 할당

a와 b와 같이 데이터 타입과 값을 할당해 주었을 때는 아무런 문제가 존재하지 않지만,

만약, a와 b에 값을 할당하지 않고 이를 접근하려 한다면, 컴파일 단계에서 오류가 발생하게 됩니다.

이를 널이 불가능한 형태의 선언이라 할 수 있습니다.

 

null이 가능한 선언 '?' (Nullable)

따라서 null을 허용하기 위한 데이터 타입이 필요합니다.

val a: Int? = null

var b: String? = null

a와 b의 데이터 타입 뒤에 '?'를 붙여주는 것으로 변수의 값에 null을 할당할 수 있습니다. 이는 a와 b에 값을 초기화하지 않고 null인 상태로 두어도 문제가 발생하지 않습니다. 이를 null이 가능한 형태의 데이터 타입이라 할 수 있습니다.

 

하지만, a와 b 같이 null이 가능한 형태의 데이터 타입을 사용할 경우, null 가능성을 체크해 주어야 합니다.

print나 println과 같은 단순 출력에는 상관이 없으나 값이 null인 상태에서 연산되는 멤버에 접근할 때 에러가 발생합니다.

val a: Int? = null
val b: Int? = 10

// 출력 가능
println("a: $a")

// 연산 불가능
val c: Int? = a + b

 

Int vs Int?, String vs String?, ...

Int와 Int?, String과 String? 등은 서로 전혀 다른 자료형(Data Type)입니다. 따라서 두 자료형 사이의 연산도 불가능함을 인지해야 합니다. 또한, Int?, String? 뿐만 아니라 Long? 등 이외의 자료형도 '?'를 붙여 사용할 수 있으니 두 가지 선언 방식을 이해해야 합니다.

fun main() {
    var a: Int = 10
    var b: Int = 5
    
    var c: Int? = 10
    var d: Int? = 5
    
    // 가능한 연산
    println(a + b)
    println(c + d)
    
    // 불가능한 연산
    println(a + c)
    println(b + d)
}

 

NPE(NullPointerException)

사용할 수 없는 null인 변수에 접근하면서 발생하는 예외로 Java나, JavaScript 등의 언어에서는 기본적으로 Null을 허용하기 때문에 자주 발생하는 예외로 유명합니다.

 

하지만 코틀린 언어에서는 기본적으로 null을 허용하지 않기 때문에 NPE를 고려하지 않고도 안전하게 프로그래밍이 가능하다고 합니다. 이를 Null Safety라 합니다.

 

또한, null을 허용하기 위해서는 자료형에 '?'를 붙여 Nullable 형태의 자료형을 사용해야 합니다.

null을 허용한 Nullable 자료형은 NPE가 발생할 수 있기 때문에 항상 널 가능성에 대한 체크를 해주어야 합니다.

 

Null Safety

Null의 존재로 인해 다양한 프로그램 언어에서는 메모리 할당에 의한 문제 또는 null 연산에 대한 오류가 많이 발생합니다. 코틀린 언어에서는 기본적으로 null을 허용하지 않는 방법으로 NPE를 고려하지 않고도 안전하게 프로그래밍을 가능하게 하며, null을 사용할 수 있는 자료형을 따로 두어 null 값을 다룰 수 있도록 구분 지었습니다.

 

즉, NPE가 발생하지 않는 프로그래밍을 지원한다는 개념을 뜻합니다.

 

세이프 콜(?.)

세이프 콜(?.)이란 nullable 자료형을 사용하여 연산할 때, 값이 null이 할당되어 있을 가능성이 있는 변수를 검사하여 안전하게 호출하도록 도와주는 연산자입니다. 사용할 변수 이름에 '?.'를 붙여서 사용할 수 있습니다.

fun main() {

    var str : String?
    str = null

    // 출력 가능
    println("str: $str")

    // 오류 발생
    println("str length: ${str.length}")

}

다음과 같이 str 값이 null인 상태에서 단순 출력에는 아무런 문제가 발생하지 않지만, 두 번째 출력문인 문자열 길이에 대해서는 에러가 발생합니다. 값이 null인 상태에서 길이를 구하려는 연산을 시도하려 했기 때문입니다.

이러한 경우를 벗어나기 위해 세이프 콜 기호 '?.'를 사용해야 합니다.

 

fun main() {

    var str : String?
    str = null

    // 출력 가능
    println("str: $str")

    // 세이프 콜(?.)을 사용하여 오류 해결
    println("str length: ${str?.length}")

}

다음과 같이 세이프 콜(?.)을 사용하여 출력하여 에러 없이 값과 길이 모두 null인 결과를 나타낼 수 있습니다.

 

Non-null 단정 기호(!!.)

non-null 단정 기호는 null이 아님을 단정하는 기호로서 컴파일러가 null검사를 하지 않고 컴파일을 할 수 있도록 하는 기호입니다. 따라서 null이 할당이 되어 있을 가능성이 있을지라도 에러 없이 컴파일을 수행하게 됩니다.

 

하지만, non-null 단정 기호(!!.)를 사용했음에도 불구하고 값이 null이라면 NPE를 발생시키므로 되도록 사용하지 않는 것을 추천드리며, 반드시 null이 아니라는 게 보장될 경우에만 사용해야 합니다.

fun main() {

    var str : String?
    str = "Hello Kotlin"

    println("str: $str")
    
    // non-null(!!.)을 사용하여 컴파일 진행
    println("str length: ${str!!.length}")

}

다음과 같이 String?으로 null 가능성이 있는 자료형이지만, 값이 null이 아님을 보장할 경우 non-null 단정 기호(!!.)를 사용하여 실행이 가능합니다.

 

엘비스 연산자(?:)

엘비스 연산자(?:)는 '?:'의 왼쪽 객체가 non-null이면, 그 객체의 값을 반환하고, null이면 '?:'의 오른쪽 값을 반환하는 기호로 null 가능성이 있는 경우 유용하게 사용되는 연산자입니다.

 

기본적으로 if 조건문을 활용하여 null 가능성을 검사할 수 있으나 엘비스 연산자(?:)를 활용하면 코드의 길이를 줄일 수 있습니다.

 

if 조건문을 사용한 경우

fun main() {
    var str : String?
    
    // null인 경우
    str = null
    
    // null이 아닌 경우
    str = "Hello Kotlin!"
	
    println("str: $str")
    
    // if 조건문을 활용한 null 검사
    if (str != null) {
        println("str length: ${str.length}")
    } else {
        println(-1)
    }
}

if 조건문을 통해 값이 null인 경우 -1, 아닌 경우 str length: 13을 출력하여 null 검사를 할 수 있습니다.

 

엘비스 연산자(?:)를 사용한 경우

fun main() {
    var str : String?
    
    // null인 경우
    str = null
    
    // null이 아닌 경우
    str = "Hello Kotlin!"
	
    println("str: $str")
    
    // 엘비스 연산자(?:)를 사용한 경우
    println("str length: ${str?.length ?: -1}")
}

엘비스 연산자(?:)를 사용하여 훨씬 간결하고 자연스러운 코드가 완성되었습니다.

 

조심해야 할 점은 엘비스 연산자(?:)를 사용하기 전, 세이프 콜(?.)을 사용하여 널 가능성 여부에 대하여 안전하게 호출할 수 있도록 한 뒤, 엘비스 연산자(?:)를 사용하여 코드를 작성한 것임을 알아두셔야 합니다.

이를 통해 null을 허용한 변수를 더욱 안전하게 사용할 수 있습니다.

 

 

 

반응형