Kotlin의 특징 중 "Null-Safety한 언어" 라는 것이 있다.
일을 하며 ?. !! ?.let 등 여러 nullable한 값에 대해 처리를 했는데, 차이점을 모르고 그냥 사용한 것 같아
공식문서를 보고 각 연산자의 특징과 왜 Null-Safety 한 언어라고 하는지 알아보았다.
지금부터 그 내용을 정리하며, 블로그에 남긴다.
NullPointerException
자바를 비롯한 대부분의 언어에서 흔히 볼 수 있는 에러는 NullPointerException 이다. Null을 참조하는 멤버에 접근하려고 했을 때 발생하는 에러이며, 공식문서에서는 NPE라고 하고 있다.
kotlin에서 NPE가 일어날 수 있는 가능성은 다음과 같다.
1. 의도적으로 NullPointerException을 발생시킬때(throw NullPointerException)
2. kotlin의 연산자 !! 을 사용했을 때(이 연산자에 대해서는 아래에서 다시 설명한다)
3. 초기화되지 않은 멤버에 접근할때.
코틀린 또한 자바 기반 언어로 JVM 위에서 실행되는 언어일텐데, 왜 kotlin은 null-safety한 언어라고 하는 것일까?
Kotlin의 nullable, non-nullable
java 언어에서는 다음과 같이 변수에 null을 넣을 수 있었다.
String nullableStr = null
kotlin 에서는 null을 넣을 수 있는 변수와 넣을 수 없는 변수가 나누어져 있다.
애초에 변수 선언 당시부터 null 이 가능한지 아닌지를 지정해주어, 변수에 예기치 못한 null이 삽입되는 것을 막아준다.
// non-nullable variable
var nonNullableStr: String = "Not Null"
nonNullableStr = null // null 이 들어갈 수 없음
// nullable variable
var nullableStr: String? = null // 타입 뒤에 ? 를 붙임으로서 nullable한 변수가 되어 null을 넣을 수 있다
nullable 이라는 말 부터가, 해석하면 "null 입력 가능" 이다.
만약, String변수인 nonNullableStr 이나, nullableStr의 길이를 구한다고 가정해보자. ".length" 를 사용한다.
nonNullableStr 은 무조건 null이 아니므로 길이를 구할 수 있을 것이다. 그러나 nullableStr의 길이를 구할때, 이 nullableStr이 null인지, 아닌지 판단해야 한다. nullableStr이 null인데 nullableStr.length를 하게 되면 NPE가 발생하게 된다.
Null-Safety
Kotlin 에서는 제공하는 연산자를 사용하여 안전하게 nullable 변수에 대해 호출하고, NPE의 발생을 최소화 할 수 있다.
먼저, 제공되는 연산자를 사용하지 않고 null을 처리하는것은, 다음과 같은 방법을 사용해왔을 것이다.
var nullableStr: String? = null
// Check Nullable
if(nullableStr != null){
Log.d(App.TAG, "Result : ${nullableStr.length}")
} else{
Log.d(App.TAG, "Result : null")
}
if문을 사용하여 nullableStr 변수가 null이 아닐 경우에만 .length를 호출하여 결과를 확인하고, null일 경우에 대해 else 처리를 해준다.
하지만 제공되는 연산자를 사용하면, 더욱 편리하게 사용 가능하다.
[1] ?. (safe call)
안전한 호출인 ?. 연산자를 사용한다. 이는 대상이 null이면 null을 리턴하고, null이 아니면 그 결과값을 리턴해준다.
Log.d(App.TAG, "Result : ${nullableStr?.length}")
대상 nullableStr이 null이면 null을 리턴해주고, null이 아닐 경우에 nullableStr.length의 결과값을 리턴한다.
여기선 nullableStr이 null이므로 null이 리턴될 것이다.
?. 없이 호출할 경우, NPE의 원인이 된다. IDE 인 안드로이드 스튜디오에서 빨간 에러 줄로 처리되며 알려주기도 한다.
만약, 여러 값이 들어있는 리스트에서, null이 아닌 값에 대해서만 어떠한 행동을 하려면 ?. 과 범위지정함수 let을 함께 사용할 수 있다.
val nullableList: MutableList<String?> = mutableListOf("hello", null, "Kotlin", null, "test")
for(i in nullableList.indices){
// nullableList[i] 가 null 이 아닐 경우에만 { } 내의 코드가 실행된다.
nullableList[i]?.let{ Log.d(App.TAG, "${nullableList[i]} is not null") }
}
[2] ?: (Elvis Operator)
엘비스 연산자라고 불리는 "?:" 는 대상이 null이 아닐땐 그대로 사용하고, null일땐 : 뒤에 나오는 값으로 사용한다는 의미이다.
이 연산자를 사용하지 않으면 다음과 같은 코드가 될것이다.
val elvisStr: String = if(nullableStr == null){
"this is Not Null"
} else {
nullableStr
}
Log.d(App.TAG, "elvis Result : $elvisStr")
elvisStr이라는 변수에 nullableStr변수가 null일 경우 "this is Not Null" 이라는 값이 들어갈 것이고, null이 아니라면 nullableStr 의 값이 들어갈 것이다.
?: 연산자를 사용하면 간략하게 사용할 수 있다.
val elvisStr = nullalbeStr?: "elvis change nullalbeStr value not null"
Log.d(App.TAG, elvisStr)
마치 null에 대한 삼항 연산자? 같은 느낌이다.(하지만 코틀린에서는 삼항연산자를 지원하지 않는다)
[3] !!
!! 연산자는 대상이 어떤 타입이던 non-nullable변수로서 취급한다.
하지만 대상이 null 일 경우엔 NPE를 발생시키니 주의해야 한다.
예를들어, nullable 변수이지만 값을 넣어주는 부분이 분명히 존재하고, 흐름상 null이 아니라고 보장할 수 있는 확실한 상황에서 nullable 변수에 대해 !! 를 사용한다.
if(/*조건*/){
nullableStr = "Not Null"
}
// 어떠한 조건에 따라서 nullableStr에 값을 넣어준다. 조건이 false 이면 nullableStr은 null일 수 있는 상황이다.
// 로직상, 조건이 참일 수 밖에 없고, null이 들어가지 않는다고 확신할 경우, 우리는 !!을 사용하여
// nullable 한 변수 nullableStr을 non-nullable한 변수라고 취급한다.
// 단, nullableStr이 null일 경우 NPE를 발생하니 주의하자.
Log.d(App.TAG, "${nullableStr!!.length}")
? 연산자를 사용한 ClassCastException 을 방지하는 방법
예외 중 ClassCastException 이라는 예외가 있다. 이는, 형 변환이 가능하지 않을 때 발생하는 Exception이다.
String 변수를 Int로 형변환하여 Int형 변수에 넣는다고 생각해보자. String형 변수는 Int로 형변환이 불가능하다.
따라서 기존엔 아래와 같이 try-catch를 사용했을 것이다.
val tmpStr = "String"
var tmpInt: Int?
try{
tmpInt = tmpStr as Int
} catch(e: ClassCastException){
tmpInt = null
}
위에서 보았던 ? 연산자와 kotlin의 형변환 as 키워드로 다음과 같이 간결하게 사용할 수 있다.
tmpInt = tmpStr as? Int
tmpStr을 Int로 형변환(as) 하는데, 가능하면? 이라는 느낌으로 생각하면 이해가 편하다. 형변환이 가능하면 변환하여 tmpInt에 넣고, 불가능하면 null을 리턴하여 tmpInt 에 null이 들어가게 된다.
참고문서
https://kotlinlang.org/docs/null-safety.html#the-operator