Null Safe

자바와 다르게 코틀린은 Null에대해 굉장히 까다롭다.

자바는 함수가 값이 Null인지 보장을 하지 못다. @Nullable, @NotNull 등의 어노테이션으로 처리를 하기는 하지만 유지보수나 가독성 측면에서 제공되는 부분이고 컴파일러가 실제로 에러를 뱉어주지는 못한다. 하지만 코틀린에서는 컴파일러가 에러를 잡아준다.

코틀린에는 안전한 호출(safe call) ?. 연산자와 엘비스 오퍼레이터라고 하는 ?: 연산자가 존재한다. (엘비스 프레슬리의 머리모양을 닮았다해서 엘비스다...)

이 ? 가 붙은경우에만 컴파일러가 Null을 허용한다.

? 가 붙지않은, Nullable 하지 않다고 되어있는 곳에 Nullable 한 값을 할당해 주려고 하면 컴파일 에러가 발생한다.

이런식으로 컴파일러가 에러를 잡아주기 때문에 런타임에서 발생할 수 있는 NPE 를 방지할 수 있다.

var str : String = "Hello World"
str = null
//error

var str_1: String? = "Hello World"
str_1 = null
//Nullable

이렇게 코드작성단에서 컴파일러가 잡아주기 때문에 협업을 할 때도 다른사람이 짠 부분의 코드에서 값을 받아야 할 때 코틀린 에서는 받아야 할 값이 null 이 올 수 있는 값인지 null 이 올 수 없는 값인지 명시적으로 알 수 있기 때문에 굳이 null 체크를 하는 코드를 추가해 줄 필요가 없어진다.

var nullable: String? = "Kotlin"
fun nullSafe(str: String?): String? {
    return str
}

Nullable 하게 선언을 했다면 그 객체에 접근을 할때도 ?. 으로 접근을 해야한다. safe-call 연산자는 이 연산자를 사용하는 객체가 null 값이 아닌 경우에 연산자 뒤의 문장을 수행한다. null 값일 경우에는 뒤의 문장을 수행하지 않고 null 값을 반환한다. -> 즉, null 인 객체의 property 를 참조하거나, 함수를 호출하는 일을 방지할 수 있다.

var str: String? = "Hello World!"
str.length
//error

str?.length
//correct

// bar가 null이 아닐 경우에만 해당 값을 대입, 그렇지 않은 경우 null을 foo에 대입한다.
val foo = bar?.baz

//foo가 null이 아닐 경우에만 bar() 호출
foo?.bar()



//java
public class Contact {

    @NotNull
    String name;

    @Nullable
    Address address;

}

class Address {

    @NotNull
    String line1;

    @Nullable
    String line2;

}

// Address.line1은 null값을 허용하지 않지만, 
// address가 null인 경우 null을 반환하게 되므로 값 line의 타입은 null값을 허용해야 한다.
val line: String? = Contact().address?.line1

// 주소가 없거나 line2가 없을 경우 기본값인 "No Address"를 리턴
val line: String = Contact().address?.line2 ?: "No Address"


str!!.length
// 절대 Null이 없을경우에 강제호출하는 방법
// 주의해야될점이 Nullable로 선언한 변수에 !!로 접근을 하면 컴파일에는 문제가 없을것이다.
// 하지만 런타임에서 이 Nullable한 값에다가 Null이 들어가는데 Null이 아니라고 !!로 접근을 하면
// 런타임중에 NPE 뿜는다.
// 고로 !!는 내가 굳이 Null이 절대 오지 않을것이다 라고 보장할 수 있는 부분에만 사용을 해야한다.

val foo: Foo? = nullable...

// 값 foo는 null값을 포함하지 않음을 보증.
val nonNullFoo: Foo = foo!!

// 값 foo가 null값이 아님을 보증하면서 bar()함수 호출
foo!!.bar()

// 값 foo가 null이 아님을 보증하면서 baz 프로퍼티 접근
foo!!.baz

Nullable한 값에 접근을 할때 Null일 경우에는 Default값을 주고싶을때 엘비스 오퍼레이터를 사용한다.

if else 로 판별해도 되지만 엘비스 오퍼레이터를 사용하는 편이 가독성측면에서 훨씬 좋다. 자바의 삼항연산자와 비슷하게 생겼다. 참고로 코틀린에는 삼항연산자가 없다.

var str: String? = "Hello World!"
str?.length ?: -1
// 앞에 값이 Null이 아니라면 그냥 Nullable의 length 값을 가져오고,
// Null이라면 Default로 설정해둔 값을 가져온다. (?: 의 뒤의 값.)
// 이런식으로 널처리를 쉽게 할 수 있다.

fun generateMapWithAddress(address: String) : Image? {
    // 검색결과가 없을경우 Exception 발생
    val postal = findPostalCode(address) ?: throw IllegalStateException()
}

as? 연산자. 지원되지 않는 자료형으로 변환을 시도하는 경우 예외가 발생한다.

val foo: String = "foo"

//java.lang.ClassCastException 발생
val bar: Int = foo as Int

//자바에서는 지원되지 않는 자료형으로 변환을 시도할 가능성이 있는 부분을
//try-catch 블록으로 감싸 처리해야 하지만,
//코틀린에선 as? (안전한변환) 연산자를 사용하여 이 문제를 간편하게 해결할 수 있다.
//as? 연산자는 형변환이 실패할 경우 예외를 발생시키는 대신에 null값을 반환한다.
//따라서 변환되는 값을 통해 변환결과를 바로 확인할 수 있다.
//null값이 올 수 있으므로 받는쪽의 자료형을 Nullable로 해주어야 한다.

val foo: String = "foo"

//bar가 null을 허용하도록 Int?로 정의한다.
//형변환에 실패하므로 bar에는 null이 할당된다.
val bar: Int? = foo as? Int

//실패의 경우 null을 반환하기 때문에 엘비스 연산자와 함께 사용할 수 있다.
val bar: Int = foo as? Int ?: 0 //형변환 실패의 경우 기본값을 0으로 지정.

자바로 작성된 클래스는 기본적으로 null 값을 허용하도록 처리되며, 코틀린에선 platform type이라 부른다. 플랫폼 타입은 Type!과 같은 형태로 표시된다. 플랫폼 타입은 코틀린에서 자바로 작성한 클래스를 사용할 때에 자동으로 지정되는 타입으로, 이 타입은 개발자가 직접 사용할 수 없다.

//java
class Person {
    String name; 
    
    public String getName() {
        return name;
    }
}
//kotlin
val person: Person = ...

// n1은 null값을 허용하지 않는다.
val n1: String = person.name

// n2는 null값을 허용한다.
val n2: String? = person.name

// 이런 특징 때문에 플랫폼타입 객체를 사용할 때는 항상 객체의 null값 여부를 확인해야만 한다. -> NPE
// 코틀린에서는 이를 해결하기 위해 자바의 annotation을 인식해 객체의 null 허용여부를 판단한다.
//java
class Person {
    @Nullable
    String name; //이런 식으로 어노테이션을 붙여주면 name필드는 코틀린에서 nullable한 프로퍼티(String?)으로 인식된다.
    
    public String getName() {
        return name;
    }
}
//kotlin
//실패
val n1: String = person.name

//성공
val n2: String? = person.name

Last updated