본문으로 건너뛰기

Nullable-Type

1 nullable type

1.1 널 가능성(nullability)

  • nullability는 NullPointerException을 피할 수 있게 돕는 코틀린 타입 시스템의 특성이다.
  • 코틀린을 비롯한 최신 언어에서 null에대한 접근 방식으로 이 문제를 가능한 실행 시점에서 컴파일 시점으로 옮기고 있다.
  • 널이 될수 있는지 여부를 타입 시스템에 추가해 컴파일러가 이 문제를 컴파일 시 미리 감지해서 실행 시점에 발생할 수 있는 예외의 가능성을 줄인다.

1.2 nullable type

자바

int strLen(String s){
return s.length()
}
  • 위 함수는 안전하지 않다.
  • null을 인자로 넘기면 NullPointerException이 발생하기 때문

코틀린: 널이 될 수 없는 타입

fun strLen(s:String) = s.length()
  • 위 자바 함수를 코틀린으로 작성해보자.
  • 이 때 중요한 부분이 함수가 널을 인자로 받을 수 있는가?이다.
  • 만약 널이 인자로 들어올 수 없다면 위와 같이 함수를 정의한다.
  • s의 타입은 String인데 이는 항상 s가 String의 인스턴스여야 한다는 뜻이다.
    • strLen에 null이 될 수 있는 인자를 넘기는 것이 금지되며 null을 넘기려고 하면 컴파일 시 오류가 발생한다.
    • 따라서 결코 strLen 함수가 실행 시점에 NullPointerException이 발생하지 않는다.
  • 그렇다면 널을 인자로 받게 하려면 어떻게 해야 될까?
    • nullable type을 사용하면 된다.
    • 아래애서 설명한다.

코틀린: nullable type

fun strLen(s:String?) = s.length()
  • 함수가 널을 인자로 받을 수 있게 하려면 위와 같이 타입 이름 뒤에 물음표(?)를 명시해야한다.
    • String 타입 뒤에 물음표(?)를 명시했다.
    • 물음표를 붙이면 해당 타입의 변수는 프로퍼티에 null 참조를 저장할 수 있다는 뜻이다.
  • nullable type의 변수가 있다면 해당 변수에 대해 수행할 수 있는 연산이 제한된다.
  • 따라서 위와 같이 s.length() 메서드를 직접 호출할 수 없다.
    • 위 코드를 컴파일 하면 오류가 발생한다.
    • 컴파일 오류를 없애려면 null 체킹이 선행되야 한다.
    • 널이 될 수 있는 타입과 null을 비교하교 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서 해당 값이 널이 될 수 없는 타입의 값처럼 사용할 수 있다.
val x:String? = null
val t:String = x
  • 또한 위와 같이 널이 될수 있는 값을 널이 될 수 없는 타입 변수에 대입할 수 없다.
strLen(x)
  • 또한 위와 같이 널이 될 수 있는 값을 널이 될 수 없는 타입의 파라미터를 받는 함수에 전달할 수 없다.

2 안전한 호출 연산자 ?.

  • 앞서 nullable type은 null 검사를 하지 않으면 연산이 제한된다고 했다.
  • null 검사에 사용할 수 있는 도구인 if는 코드가 번잡해지는 일을 피할 수 없다.
  • 따라서 코틀린은 null 검사를 간단하게 지원하는 연산자를 지원한다.
  • 코틀린에서 제공하는 ?.연산자는 null검사와 메서드 호출을 한 번의 연산으로 수행한다.
    • 호출하려는 값이 null이 아니리면 ?.는 일반 메서드 호출과 같고 null인 경우 호출은 무시되고 null이 결과 값이 된다.
    • s?.toUpperCase()if (s != null) s.toUpperCase() else null과 같다.

3 엘비스 연산자 ?:

  • 코틀린은 null 대신 디폴트 값을 지정할 때 편리하게 사용할 수 있는 엘비스 연산자를 제공한다.
  • 코틀린에서는 return, throw 등의 연산도 식이기 때문에 엘비스 연산자의 우항으로 사용할 수 있다.

예시

  • 엘비스 연산자는 이항 연산자로 좌항 값이 null이 아니면 좌합 값을 결과로 하고 좌항 값이 널이면 우항 값을 결과로 한다.
fun strLenSafe(s: String?): Int = s?.length ?: 0

fun main(args: Array<String>) {
println(strLenSafe("abc"))
println(strLenSafe(null))
}
3
0

4 안전한 캐스트 as?

  • as 연산자는 자바 타입 캐스트와 마찬가지로 대상 값을 as로 지정한 타입으로 바꿀 수 없으면 ClassCastException이 발생한다.
    • 따라서 as를 사용하기 전에 is 연산자로 미리 변환 가능한 타입인지 검사를 할 수 있다.
    • 하지만 간결함을 지향하는 코틀린에서는 as? 연산자를 사용해 검사와 변환을 한번에 할 수 있다.
  • as? 연산자는 값을 대상 타입으로 변환할 수 없으면 null을 반환한다.
  • as? 연산자를 사용하는 일반적인 패턴으로 캐스트를 수행한 후에 엘비스 연산자를 사용하는 것이다.

5 널 아님 단언 !!

  • !! 연산자를 사용하면 어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다.
    • 실제 널에 사용하면 NPE가 발생한다.
  • !! 연산자는 "나는 이 값이 null이 아님을 잘 알고 있다. 내가 잘못 생각했다면 예외가 발생해도 감수하겠다"라는 의미다.
  • !! 연산자를 사용해서 발생하는 예외에는 어떤 식에서 예외가 발생했는지에 대한 정보가 없다.
    • 따라서 여러 !! 연산자를 한 줄에 함께 쓰는 일을 피하자

6 let 함수

  • let 함수를 ?.(안전한 호출 연산자)와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음 결과를 변수에 넣는 작업을 간단히 처리할 수 있다.
  • let을 사용하는 가장 흔한 경우로 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우다.

let 함수

  • let 함수는 코틀린에서 매우 유용한 기능으로, 특히 널(null) 안전성을 다룰 때 자주 사용됩니다.
  • let 함수는 객체를 받아서 lambda 함수 내에서 이 객체를 사용할 수 있게 해줍니다.
  • lambda 함수 내에서 객체는 it이라는 이름으로 참조됩니다.
  • let 함수는 lambda 함수의 결과를 반환합니다.

예시

fun sendEmailTo(email: String) {
println("Sending email to $email")
}
  • 이 함수는 null이 아닌 문자열만 받을 수 있습니다.
  • 그런데 우리가 가진 이메일 주소가 null일 수도 있다면 어떻게 해야 할까요?
if (email != null) sendEmailTo(email)
  • 따라서 이 함수에 널이 될 수 있는 타입의 값을 넘길 수 없고 인자를 넘기기 전에 위와 같이 널 검사를 해야합니다.
fun main(args: Array<String>) {
var email: String? = "yole@example.com"
// null이 아니기 때문에 sendEmailTo가 호출된다.
email?.let { sendEmailTo(it) }


email = null
// null이기 때문에 아무 일도 일어나지 않는다.
email?.let { sendEmailTo(it) }
}
  • let 함수를 사용하면 코드가 더 간결해집니다.
  • ?.(안전 호출 연산자)와 함께 사용하면, email이 null이 아닐 때만 let 블록이 실행됩니다.

7 플랫폼 타입

  • 자바 타입 시스템은 널 가능성을 지원하지 않는다.
  • 자바 코드에도 애노테이션으로 표시된 널 가능성 정보가 있다.
    • 코틀린은 이런 애노테이션이 있으면 그 정보를 활용한다.
    • 따라서 자바의 @Nullalbe String은 코틀린에서 볼 때 String?과 같고 자바의 @NotNull String은 코틀린에서 볼 때 String과 같다.
    • 코틀린은 JSR-305 표준 애노테이션, 안드로이드, 젯브레인스 도구들이 지원하는 애노테이션에서 널 가능성 정보를 읽는다.
  • 자바에서 이런 널 가능성 애노테이션이 없는 경우 자바의 타입을 코틀린에서는 플랫폼 타입이라고 한다.
    • 즉 플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다.
  • 코틀린에서 플랫폼 타입은 널이 될 수 있는 타입이나 널이 될 수 없는 타입 모두로 사용할 수 있다.
    • 컴파일러는 플랫폼 타입에 대해서 모든 연산을 허용한다.
    • 즉 플랫폼 타입에 대해 수행하는 모든 연산의 책임은 개발자가 져야한다.