안드로이드/Kotlin

[Kotlin] Null safety

devGeek 2022. 10. 30. 17:15
반응형

Null Safety

Nullable types and non-null types

Kotlin 타입 시스템은 null 참조의 위험성을 제거하는데 초점이 맞혀있다

Java를 포함하여 많은 프로그래밍 언어에서 가장 흔한 위험 중 하나는 null 값을 갖는 멤버에 접근함으로써 null reference exception을 초래하는 것이다.
Java 에서는 이런 경우를 NullPointException 이나 줄여서 NPE로 부른다.

Kotlin에서 NPE(NullPointException)이 발생하는 경우는 아래와 같다.

  • throw NullPointerException()을 명시적으로 호출하는 경우.
  • non-null assertation 연산자 !!를 사용한 경우
  • 특정 변수를 생성자에서 초기화하지 않고 해당 변수를 다른 곳으로 보내거나 사용하는 경우.
  • 특정 클래스를 상속 받아서 사용하는데 부모 클래스에서 선언된 open member를 초기화 하지 않은 상태에서 부모 클래스의 생성자가 동작되고 해당 생성자에서 자식 클래스에서 초기화 하지 않은 멤버를 사용하는 경우.

Kotlin에서는 타입 시스템이 참조 변수의 nullable 형태를 판단한다. 다시 말해 해당 변수가 null를 갖을 수 있는지 또는 null값을 갖을 수 없는지를 판단한다. 예를 들어 설명한다면 일반적으로 String 타입의 변수는 null를 갖을 수 없다.

var a: String = "abc" // Regular initialization means non-null by default
a = null // Compile Error

위에서 null 값을 갖도록 허용하기 위해서는 직접 변수를 선언할 때 타입에 nullable 하다고 명시적으로 String?으로 선언해야한다.

var b: String? = "abc"  // can be set to null
b = null // OK
print(b)

이제는 만약 a에 접근하게 되면 NPE를 발생시킬 위험성이 없음을 보장하기 때문에 자유롭게 a를 사용할 수 있다.

val l = a.length

하지만 만약 b에 접근하려고 한다면 non-null이 보장되지 않기 때문에 b를 그냥 사용할 경우 컴파일러에서 에러를 나타낼 것이다.

val l = b.length  // error: variable `b` can be null

하지만 우리는 b와 같은 변수나 객체에 접근할 수 있어야 한다.
그래서 아래에서는 몇 가지 방법들을 설명하려고 한다.

Checking for null in conditions

먼저 우리는 명시적으로 bnull인지 체크할 수 있다.

val l = i f (b != null) b.length else -1

그러면 컴파일러는 우리가 null 체크에 대한 정보를 트랙킹하여서 b가 non-null 인 경우 blength를 호출한다.
조금더 복잡하게 nullable에 대해 체크한다면 다음과 같이 할 수 있다.

val b: String? = "kotlin"
if (b != null && b.length > 0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}

Safe calls

nullable한 변수에 접근할 수 있는 다른 방법은 안전한 호출(safe call) 연산자 ?.를 사용하는 것이다.

val a = "Kotlin"
val b: String? = null
print(b?.length)
print(a?.length)  // Unnecessary safe call

print(b?.length)에서 bnull이기 때문에 safe call 연산자 ?. 덕분에 length를 호출하지 않고 b?에서 null를 반환한다.

safe call 연산자는 체인형태에서 유용하다. 예를 들어 Bob라는 회사원이 있는데 그는 특정 부서(department)에 속해 있고 그 부서에는 부서장(head)이 있고 부서장의 이름(name)을 알아야 한다몀 다음과 같이 코드를 작성해야 할 것이다.

bob?.department?.head?.name

이런 체인형태의 코드에서 만약 어느 한 속성값이 null이라면 null를 반환하게 된다.

만약 non-null인 경우에만 변수(객체)를 이용하여 연산을 수행한다면, 우리는 let 연산자와 함께 safe call 연산자 ?.를 사용할 수 있다.

val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let { println(it) } // prints Kotlin and ignores null
}

그리고 safe call은 변수에 값을 할당할 때, 좌항(저장될 변수)에서도 사용될 수 있다.
만약에 체인 형태로 된 리시버(변수)에 null를 포함하고 있다면 값 할당은 무시하게 된다. 우항에 있는 코드는 동작은 무시된다.

// If either 'person' or 'person.department' is null, the function is not called
person?.department?.head = mansgersPool.getManager()

Elvis Operator

우리가 nullable를 참조해야하는 경우, b처럼, 우리는 "bnull이 아니라면 이것을 null이면 non-null 값을" 형태로 처리할 수 있다.

val l: Int = if (b != null) b.length else -1

하지만 if 표현식을 사용하지 않는 대신에 우리는 Elvis Operator(연산자) ?:를 이용할 수 있다.

val l = b?.length ?: -1

?: 연산자의 왼쪽에 있는 값이 null이 아니라면 Elivis 연산자는 왼쪽(b.length)에 있는 값을 반환한다.
반면에 왼쪽에 있는 값이 null이라면 오른쪽(-1)에 있는 값을 반환한다.
여기서 주의해야 할 것은 Elvis 연산자(?:) 우측에 나오는 표현식은 좌측에 있는 값이 null인 경우에만 연산된다.

The !! operator

세번째 방법으로는 non-null asseration 연산자 !!를 사용하는 것이다.
!! 연산자는 해당 값이 non-null 타입이라고 강제하는 것이고 만약 null인 경우에는 에러를 던진다.
사용방법은 b!!와 같이 쓸 수 있고 b가 가지고 있는 값이 non-null이 아님을 강제하고 만약 null인 경우에는 NPE를 던진다.

val l = b!!.length

Safe casts

일반적으로 캐스팅을 하는 경우 캐스팅 할 수 없는 타입이라면 ClassCastException 에러가 발생시킨다.
안전하게 캐스팅하는 방법은 ClassCastException을 발생시켜 앱이 죽기 보다는 null를 반환하도록 하는 것이다.
캐스팅 할 떄 as?와 같이 ? 연산자를 추가하면 된다.

val aInt: Int? = a as? Int

Collections of a nullable type

만약 nullable한 값을 갖는 컬렉션을 사용하는 경우에 non-null 원소만 필터링하려고 한다면 다음과 같이 filterNotNull를 이용할 수 있다.

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
질문과 잘못된 점에 대해 말씀해주시는 건 언제나 환영입니다.
zero5.two4@gmail.com
반응형