[Kotlin In Action] 6-1) null 가능성(nullability)과 safe call, 엘비스(elvis) 연산자

ERyukSa·2023년 6월 28일
0

Kotlin In Action

목록 보기
8/9

개요

널 가능성(nullability)은 NullPointerException(NPE) 오류를 피하는데 도움이 되는 코틀린 타입 시스템의 특성이다.

null이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 미리 null 문제를 감지하여 실행 도중의 NPE 발생 가능성을 줄일 수 있다.

null이 될 수 있는(nullable) 타입

코틀린의 타입 시스템은 자바와 달리 null이 될 수 있는 타입과 없는 타입을 명시적으로 구분한다.

var nonNullStr: String = "abc" // null 불가능한 타입
var nullableStr: String? = "abc" // null 가능한(non-null) 타입

둘은 같은 문자열이지만 위에 변수엔 null을 저장할 수 없고, 아래는 가능하다.

nonNullStr = null // 컴파일 에러!

이처럼 어떤 타입이든 뒤에 ?를 붙이면 변수에 그 타입 객체와 null을 저장할 수 있으며, nullable 타입이라고 한다.

nullable 타입의 제약

nullable 타입엔 제약 사항이 있다. 일반적인 방식으로 프로퍼티나 함수를 호출할 수 없고, non-null 타입의 변수나 파라미터에 대입할 수도 없다.

따라서, 아래의 코드는 모두 컴파일러가 허락하지 않는다.

// 컴파일 에러1
fun strLen1(str: String?) = str.length

// 컴파일 에러2
val x: String? = "abc"
val y: String = x // non-null 타입에 nullable 변수 값 할당 불가능

// 컴파일 에러3
fun strLen2(str: String) = str.length
strLen2(x) // non-null 파라미터에 nullable 인자 전달 불가능

이렇게 제약이 많은데 nullable로 대체 뭘 할 수 있을까?

우선, if문 검사를 통과한 변수는 특정 범위 안에서 non-null로 사용할 수 있다.

fun strLenSafe(str: String?): Int =
	if (str != null) str.length else 0

그런데 nullable 타입을 다루는 도구가 if문 뿐이면 다루기 번거롭고 코드가 지저분해질 것이다. 당연하고 다행히도 코틀린은 nullable 타입을 다루는 여러 도구를 지원한다.

Safe call (안전한 호출): ?.

nullability를 다루는 가장 유용한 연산자는 safe call이라고 불리는 ?.이다. ?.는 객체와 메서드(프로퍼티) 사이에 위치하는데, null 검사와 메서드 호출을 함께 수행한다.

fun printAllCaps(s: String?) {
	val caps: String? = s?.toUpperCase()
    println(caps)
}

s?.upUpperCase()에서 safe call이 사용되었다. 이것은 훨씬 더 긴 if (s != null) s.toUpperCase() else null과 같은 동작을 한다. s가 null이 아니면 일반 메서드 호출처럼 동작하고, null이면 메서드를 무시하고 결과 값이 null이 된다.

>>> printAllCaps("abc")
ABC
>>> printAllCaps(null)
null

safe call 연쇄 호출

safe call은 null이 될 수 있는 중간 객체가 여러 개 있을 때 연쇄 호출해서 편하게 null 처리를 할 수 있다.

class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val address: Address?)
class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
	val country = this.company?.address?.country
    return if (country != null) country eles "Unknown"
}

>>> println(Person("홍길동", null).countryName())
Unknown

위에서 val country = this.company?.address?.country를 보면 ?.를 연쇄하여 추가 검사 없이 한 줄로 null을 처리하고 있다.

엘비스(elvis) 연산자: ?:

코틀린은 어떤 식이 null일 경우 대신 사용할 default 값을 지정할 수 있는 연산자를 제공한다. 그것을 엘비스 연산자라고 하며 ?:로 생겼다.

fun strLenSafe(s: String?): Int = s?.length ?: 0

>>> println(strLenSafe("abc"))
3
>>> println(strLenSafe(null))
0

s?.length ?: 0에서 s?.length가 null이 아니면 s?.length이 결과 값이 되고, null이면 ?: 뒤의 0이 전체 식의 결과가 된다.

참고로 ?:를 시계 방향으로 90도 돌리면 엘비스 프레슬리의 헤어스타일과 비슷하다는 이유로 엘비스 연산자라고 한다. (ㅋ)

엘비스 연산자를 이용하면 위에서 봤던 Person.countryName 함수를 한 줄로 표현할 수도 있다.

fun Person.countryName(): String =
	this.company?.address?.country ?: "Unknown"

함수의 전제 조건 검사

?:는 return, throw와 함께 함수의 전제 조건을 검사할 때 유용하다.

fun printAddress(person: Person) {
	val address = person.company?.address ?: throw IllegalArgumentException("No address")
    println(address)
}

정리

  • 코틀린은 런타임에 발생하는 NPE 가능성을 줄이기 위해 null 가능 여부를 타입 시스템에서 지원한다.
  • nullable 타입은 함부로 객체의 메서드, 프로퍼티를 호출할 수 없고, non-null 변수 자리에 nullable 변수를 사용할 수 없는 등 안전한 만큼 제약 사항이 있다.
  • nullable 타입은 대표적으로 if문 null 검사, safe call, elvis 연산자를 이용하여 제약 사항에 손쉽게 대응할 수 있다.

0개의 댓글