본문 바로가기

Kotlin

[Kotlin] Null Safety

 

안녕하세요.

이번에 회사에서 Java 언어를 Kotlin으로 바꾸면서 고민하고 찾아봤던 내용을 기반으로 작성했습니다.

 

그 중 Null 처리에 관해 고민을 많이 했었습니다.

그래서 오늘은 Kotlin의 Null Safety에 대해 알아보고 어떻게 이를 적용했는지 알아보겠습니다.


Null 처리

기존 자바 코드의 NullException을 캐치하는 try - catch 문을 그대로 코틀린으로 가져와서 Not-Null 연산만 덧붙이고 끝내고 싶었습니다.

fun printModelLength(model: String?) {
    try {
        val length = model!!.length
        println("The length of the model string is: $length")
    } catch (e: NullPointerException) {
        println("Caught NullPointerException: model is null")
    }
}

 

하지만 이건 코틀린에서 제시하는 Null Safety의 이점을 하나도 가지지 못하는 코드입니다.

코틀린의 Null Safety 특성을 어떻게 녹여낼지 고민했습니다.


Kotlin에서 Null을 처리하는 방법

코틀린이 제공하는 null 관련 문법은 뭐가 있는지 찾아봤고,

null을 안전하게 처리하는 가이드라인이 있지 않을까 싶어 알아봤습니다.

 

Null 처리 가이드라인

일단 null이 아니면 best 입니다.

  • Nullable 변수를 Non-Null 변수로 바꿀 수 있다면 good
  • Null Object Pattern을 이용하자

Null Object Pattern

null을 반환하는 대신, 빈 리스트나 기본 값을 지닌 컬렉션을 반환하는 디자인 패턴입니다.

 

before

null을 반환하는 함수가 있습니다.

fun getSysStream(model: String?): List<String>? {
    return model?.let {
        listOf("Stream1", "Stream2")
    }
}

 

After

null 대신 빈 리스트를 반환합니다.

fun getSysStream(model: String?): List<String> {
    return model?.let {
        listOf("Stream1", "Stream2")
    } ?: emptyList()
}

 

이렇게 하면 getSysStream을 호출하는 곳에서 null 처리 걱정을 안해도 됩니다.

별도의 try-catch를 사용하지 않아도 됩니다.

 

이렇게 처리할 수가 없고, null을 가지는 변수라면 Kotlin의 Null Safety를 사용하면 됩니다.


Null Safety 문법

Safe Calls

?. 키워드를 사용합니다.

Nullable 변수를 다룰 때 안전하게 호출할 수 있도록 돕는 연산자 입니다.

  • null이 아닐 때 수행, null이면 null 반환
val length: Int? = name?.length

 

Elvis Operator

?: 키워드를 사용합니다.

변수가 null인 경우 대체 값을 제공할 수 있습니다.

  • 아래 코드에서 null인 경우 0을 반환한다는 의미입니다.
val length: Int = name?.length ?: 0

 

Safe Cast

as? 키워드를 사용합니다.

널 가능 캐스트를 수행하여 캐스트가 성공하면 해당 타입을, 실패하면 null 을 반환합니다.

fun main() {
    val model: Any = "Kotlin"

    val str: String? = model as? String
    println(str) // 출력: Kotlin

    val num: Int? = model as? Int
    println(num) // 출력: null
}

 

let

let 함수를 사용하여 블록 내에서 널이 아닌 변수로 처리할 수 있습니다.

null이라면 해당 블록을 수행하지 않고 null을 반환합니다.

  • nullable 변수를 여러 번 사용할 때 유용합니다.
name?.let {
    println(it.length)
    println(it.uppercase())
}

 

Safe Collection Access

컬렉션에서 안전하게 값을 가져올 수 있는 메서드 입니다.

val list = listOf("Kotlin", "Java")
val firstElement: String? = list.getOrNull(0)
val secondElement: String? = list.getOrNull(1)
val thirdElement: String? = list.getOrNull(2) // 널 반환

마주한 문제

null safety 연산을 사용하려 보았는데, 선뜻 바꾸지 쉽지 않은 부분이 많았습니다.

이러한 문제에 직면했을 때 여러 참고자료를 찾아보고, 실제 다른 파트의 코드로 확인해보면서 수정했습니다.


1. 같은 변수에 계속 ?. 붙임

Nullable 변수를 호출할 때마다 매번 ?. 을 붙이는게 번거롭습니다.

이를 ?. 연산자와 let으로 처리했습니다.

 

before

매번 ?. 을 붙여줬습니다.

fun process(input: String?) {
    println("The length of the string is: ${input?.length ?: 0}")

    if (input?.isNotEmpty() == true) {
        println("The string is not empty")
    }

    println("The string in uppercase is: ${input?.uppercase() ?: ""}")
    println("The first two characters are: ${input?.substring(0, 2) ?: ""}")
}

 

after

?.let 으로 블록 안에는 non null 변수가 확실해서 ?.을 붙일 필요가 없습니다.

fun process(input: String?) {
    input?.let {
        println("The length of the string is: ${it.length}")

        if (it.isNotEmpty()) {
            println("The string is not empty")
        }

        println("The string in uppercase is: ${it.uppercase()}")
        println("The first two characters are: ${it.substring(0, 2)}")
    }
}

2. throw NullException

?. 연산자와 let으로 throw를 던져 if문을 깔끔하게 리펙토링 했습니다.

 

before

fun process(input: String?) {
    if(input == null) {
        throw NullPointerException("input is null")
    }

    println("The length is ${input.length}")
    println("The length's uppercase is ${input.uppercase()}")
}

 

after

fun process(input: String?) {
    input?.let {
        println("The length is ${it.length}")
        println("The length's uppercase is ${it.uppercase()}")
    } ?: throw NullPointerException("input is null")
}

3. Nullable 타입 변환 후 계속 ?. 붙임

Safe Cast와 Elvis Operator를 사용해 처리했습니다.

 

before

fun process(input: Any?) {
    val str = input as? String

    println("The length of the string is: ${str?.length ?: 0}")

    if (str?.isNotEmpty() == true) {
        println("The string is not empty")
    }

    println("The string in uppercase is: ${str?.uppercase() ?: ""}")
    println("The first two characters are: ${str?.substring(0, 2) ?: ""}")
}

 

after

fun process(input: Any?) {
    val str = input as? String
        ?: throw NullPointerException("Input is null")

    println("The length of the string is: ${str.length}")

    if (str.isNotEmpty()) {
        println("The string is not empty")
    }

    println("The string in uppercase is: ${str.uppercase()}")
    println("The first two characters are: ${str.substring(0, 2)}")
}

참고 자료

https://www.dhiwise.com/post/kotlin-null-safety-a-comprehensive-guide-for-developers

 

A Deep Dive into Kotlin Null Safety Mechanisms

Learn how Kotlin null safety makes code safer and your apps more stable.

www.dhiwise.com

https://product.kyobobook.co.kr/detail/S000001033129

 

이펙티브 코틀린 | 마르친 모스칼라 - 교보문고

이펙티브 코틀린 | 실제 개발 사례를 통해 알려주는 코드 품질 향상 전략이 책은 더 나은 코틀린 개발자가 될 수 있도록 도움을 주는 안내서입니다. 코틀린에 어떤 기능이 있는지, 어떤 표준 라

product.kyobobook.co.kr

 

 

 

 

 

'Kotlin' 카테고리의 다른 글

[Kotlin] Sealed Class  (3) 2024.09.22
[Kotlin] Extension Function (확장 함수)  (0) 2024.09.10
[Kotlin] Coroutine (3) - 예외 처리  (1) 2023.12.01
[Kotlin] Coroutine (2) - Use in Kotlin  (0) 2023.11.30
[Kotlin] Coroutine (1) - 코루틴이란?  (0) 2023.11.30