본문 바로가기

Android/Kotlin

코틀린 널에 대해서

코틀린에 대해서 처음에 언급하는 부분이 NullPointerException(이하 NPE)를 없애기 위해 노력하여, 널 안전성이 높다는 것이다.

코틀린을 비롯한 최신 언어에서 널에 대한 접근 방법은 실행 시점에서 컴파일 시점으로 옮기는 것이다.

널이 될 수 있는지 여부를 컴파일러가 미리 감지해서 실행 시점에 발생하는 오류의 가능성을 줄일 수 있는 것이다.

 

코틀린에서는 널이될 수 있는 타입을 명시적으로 표현할 수 있다.

자바의 경우를 보면, 어떤 변수가 널이 될 수 있다면 그 변수에 대해 메서드를 호출하게 되면 NPE가 발생할 수 있으므로 안전하지 않다.

반면 코틀린에서는  NPE가 발생할 수 있는 함수 호출을 금지하기 때문에 많은 오류를 방지할 수 있다.

자바와 코틀린에서 널 처리에 대한 코드를 보자.

//Java
void showNameLength(String name) {
    println(name.length() //name이 null일 경우 에러 발생
}

//Kotlin
fun showNameLength(name: String) {
    println(name.length() //name은 null을 허용하지 않기 때문에 null을 넣을 경우 컴파일 에러 발생
}

안전한 호출 ?.

코틀린 함수에서 name은 null을 허용하지 않는 String 객체이다. null을 넣었을 경우 컴파일 오류가 발생하기 때문에 결코 실행 시점에 NPE를 발생시키지 않는다고 장담할 수 있다.

name이 null을 받을 수 있으려면 물음표(?)를 명시해야 한다.

Int?, Boolean?, String?, CustomClass? 등 어떤 타입이든 뒤에 물음표를 붙이면 null을 허용한다는 뜻이다.

fun showNameLength(name: String?) = name.length() //널체크가 없으므로 컴파일 에러 발생

val s: String? = null
val t: String = s //컴파일 에러 발생 null을 허용하지 않으므로 null 객체 대입 불가

fun showNameLength(name: String?) {
    if (s != null) {
        println(s.length()) //null체크를 하면 컴파일된다.
    }
}

널 체크를 하면 컴파일이 무사히 통과된다. 하지만 널을 다루기 위해 사용할 수 있는 도구가 if를 이용한 검사뿐이라면 코드가 번잡해지는 것은 피할 수 없다. 왠지 코틀린스럽지 않다. 코틀린은 널을 다루는 여러 도구를 제공한다.

코틀린에서 제공하는 가장 유용한 도구가 안전한 호출 연산자 ?. 이다. ?.은 null검사와 동시에 함수 호출을 한 번의 연산으로 수행한다.

아래 코드를 보자.

//case 1
println(name?.length())

//case 2
if (name != null) {
    println(name.length())
} else {
    null
}

case1은 case2와 같다. 코틀린스럽게 코드가 간략하다.

?.는 name이 null이 아니라면 함수가 동작한다. name이 null이라면 이 호출은 무시가 되고 null이 된다.

 

널 체크가 들어간 호출이 연달아 있는 경우가 있을 수 있다.

아래코드를 보자.

class Member(val car: Car?)
class Car(val maker: Maker?)
class Maker(val carName: String?, val carType: String?)

fun Member.carName(): String {
    val carName = this.car?.maker?.carName //안전한 호출 ?.를 연속해서 사용한다.
    
    if (carName != null) {
        return carName
    } else {
        return "No Match"
    }
}

코틀린에서는 안전한 호출 ?.를 다중으로 사용할 수 있어서 훨씬 간결하게 널 검사를 할 수 있다.

마지막 코드에 if 로 널 체크를 해서 리턴 값을 반환하고 있는데 코틀린에서는 이러한 if문도 없앨 수 있다.

 

엘비스 연산자 ?:

코틀린은 null 대신 사용할 값을 지정할 때 엘비스 연산자 ?:를 제공한다. 엘비스 프레슬리 특유의 헤어스타일과 닮았다고 해서 엘비스 연산자라 불리운다. 아래 코드는 엘비스 연산자를 사용하는 방법이다.

fun showNameLength(name: String?) {
    val nameLength: String = name?: "noname"
}

엘비스 연산자는 이항 연산자로써 좌항을 계산한 값이 널인지 검사하여 널이 아니라면 좌항 값을 결과로 하고 널이라면 우항 값을 결과로 한다. 코틀린스럽게 코드를 아주 간결하게 처리할 수 있다.

 

안전한 캐스팅 as?

이번에는 안전한 캐스트 as?를 알아보자.

as? 연산자는 어떤 값을 지정한 타입으로 캐스팅한다. 지정한 타입으로 캐스팅이 불가할 경우 null을 반환한다.

as?를 사용할 때도 캐스팅을 수행한 뒤 엘비스 연산자를 사용한다.

val isSame = car as? Member?: return false //타입이 서로 일치 하지 않으면 false

 

널이 아닙니다 !!

!!는 코틀린에서 널이 될 수 있는 값에 대해서 느낌표 두개 !!를 사용하면 어떤 값이는 널이 아닌 값으로 강제로 바꿀 수 있다.

아래 코드를 보자.

fun showNameLength(name: String?) {
    val nameLength: String = name!! //name이 null이면 NPE가 발생한다.
}

!!는 예외가 발생해도 내가 감수하고 쓰겠다는 의미가 된다. 느낌표 두개 !!가 뭔가 주의하라는 느낌을 준다. 이는 의도한 것이라고 한다. 컴파일러가 검증할 수 없는 !!를 사용하기 보다 더 나은 방법을 찾아보라고 주의를 주는 의미로 !!를 택했다고 한다.

 

let 함수

그 값이 널이 아닐 때, 수행해야 할 로직이 있을 때 let을 사용하면 편리하다.

val name: String? = "홍길동"
name?.let {
  ....
  ....
  ....
}