안녕하세요.
이번에 회사에서 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
https://product.kyobobook.co.kr/detail/S000001033129
'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 |