본문 바로가기

코틀린

Null safety

Nullable types and non-nullable types

(null 이 가능한 타입과 불가능한 타입)
 
null 이 가능한 타입에 접근하는 방법
  1. ? 연산자 사용
  2. ?. 연산자 사용
 
코틀린타입의 목적은 객체가 null 이 된는 것을 방지하는 것을 목적으로 하고 있다.
(The Billion Dollar Mistake 이 아저씨가 말한대로 말이다.)
 
자바를 포함해서 수많은 프로그래밍언어에서 위험한 내용은 null인 객체의 멤버에  접근하는 것이고 이는 객체 null 예외를 발생시킨다. 자바에서는 NullPointerException 과 같고 줄여서 NPE 라고 부른다.
 
아래는 코틀린에서 NPE가 발생할 수 있는 유일한 가능성들이다.
  • 확실하게 throw NullPointerException() 를 부르기
  • 아래에서 설명한 !! 연산자를 사용하기
  • 데이터를 초기화할때 내용이 다른경우, 예를들면 :
    -  생성자에서 사용할 수 있는 초기화되지 않은 this가 전달되어 어딘가에 사용될때("this 가 leaking 될때).
    - A superclass constructor calls an open member whose implementation in the derived class uses an uninitialized state.
    (슈퍼클래스 생성자는 파생 클래스에서 초기화되지 않은 상태를 사용하는 구현을 가진 개방형 멤버를 호출합니다.)
  • 자바 연동:
    - platform type(Java 선언의 유형은 Kotlin에서 특정 방식으로 처리되며 이를 platform type이라고 한다. )의 null객체의 멤버에 접근하려고 하는 경우
    - 제네릭타입을 자바에 연동해서 사용할때 null이 될 가능성이 있다. 예를 들어서, 자바 코드가 코틀린의 MutableList<String> 에 null을 추가할 수 있고, MutableList<String?> 을 호출하는 것은 null이 될 가능성이 있다.
  • 다른 문제들이 null이 발생하는 원인은 외부의 자바코드 때문이다.
 
코틀린에서, 타입 시스템은 null을 가질 수 있는(nullable references) 객체와 null을 가질 수 없는(non-nullable references) 객체를 구별한다.
// Regular initialization means non-nullable by default
var a : String = "abc"  
a = null //compile error

 

null을 허용하기 위해서는 변수뒤에 물음표를 선언하면 된다. String? 이렇게.
var b : String? = "abc"
b = null //ok
print(b)
이제 메소드를 호출하거나 a의 프로퍼티에 접근할때, NPE가 발생하지 않는게 보장이 됐고, 아래와 같은 코드를 안전하게 사용할 수 있다.
val l = a.length
하지만 b에서 똑같은 프로퍼티에 접근을 원하면, 이는 안전하지 않고, 컴파일러는 에러를 뱉을 것이다.
val l = b.length // error: variable 'b' can be null
하지만 우리는 객체 b 의 프로퍼티에 접근해야한다. 이를 위한 방법이 있다.
 

Checking for null in conditions

(조건에서 null을 확인하기)
 
첫째, b가 null인지 확실하게 확인하고, 두개의 옵션을 나눠서 관리해라 :
val l = if (b != null) b.length else -1
컴파일러는 당신이 수행했던 정보를 추적하고, length를 if안에서 호출하는 것을 허락한다.
더욱 복잡한 조건이 가능하다 :
val b : String? = "Kotlin"
if(b != null && b.length >0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}
이 방법은 b가 불변인 경우에만 작동하며, 그렇지 않은 경우 검사 후 b가 null로 변경될 수 있다.

Safe calls

null이 가능한 변수에 접근하는 두번째 방법은 ?. 연산자를 사용하는 것이다.
그리고 let 연산자를 사용하는 것도 하나의 방법이다.
val a = "Kotlin"
val b : String? = null
println(b?.length)
println(a?.length)     // Unnecessary safe call
Safe calls는 chains 를 사용할때 유용하다.  예를들어서 Bob이라는 사람은 편의점에서 일하는 고용인이다.(아닐수도 있고). 이 편의점은 점주로서 다른 고용인을 갖고있다. Bob의 가게 이름(있는 경우)을 얻으려면 다음과 같이 작성합니다.
bob?.department?.head?.name
이 값중 하나라도 null 이 발생하면 chain은 null을 반환한다.
non-null 값만을 쓰기 위한 연산자는 안전한 ghcnfdls let 을 쓰면 된다.
(아래 코드 스니펫을 보면 let연산자를 사용했다.
varl listWithNulls : List<String?> = listOf("Kotlin", null)
for( item in listWithNulls){
    item?.let {println(it)}    //prints Kotlin and ignores null
}
안전한 호출은 값 할당의 좌측에서 쓸 수 있다. 안전한 호출 chains에 있는 receiver(수신자)가 null이면 (아까 Bob 같은 경우를 떠올리면 될듯), 값 할당은 건너뛰어지고 우측의 표현식은 동작되지 않는다.
// If either `person` or `person.department` is null, the function is not called:
person?.department?.head = managersPool.getManager()

Nullable receiver

확장함수는 null이 가능한 수신자에 의해 정의될 수 있다. 이렇게 하면 각 call - site에서 널 검사 로직을 사용할 필요 없이 널 값에 대한 동작을 지정할 수 있습니다.
예를 들면, toString() 메소드는 null이 가능한 수신자에서 정의된다. 문자열 값 "null"을 리턴해준다.
이는 특정한 상황에 도움이 되고 아래와 같이 로그를 찍는 상황이다.
val person: Person? = null
logger.debug(person.toString()) // Logs "null", does not throw an exception
toString() 메소드가 null이 가능한 String을 리턴받기 원한다면, 안전한 호출자인 ?. 연산자를 사용해라.
var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // Returns a String? object which is 'null'
if(isoTimestamp == null){
    //timestamp가 null일때 처리
}

Elvis operator

null이 가능한 객체 b가 있는 경우 "b가 널이 아니면 사용하고, 그렇지 않으면 null이 아닌 값을 사용" 한다는 뜻이다. 쉽게 얘기하면 왼쪽값이 null이 아니며 그걸로 쓰고 만약 왼쪽값이 null이다? 그러면 오른쪽 값을 쓰는거다.
val l: Int = if(b!=null) b.length else -1
위에 예문처럼 조건문으로 작성하는 대신에 일베스 연산자 :? 를 사용해서 표현하는게 가능하다.
val l = b?.length ?: -1
?: 연산자를 썼을때 좌항이 null이 아니면, 엘비스 연산자(?:)는 좌항의 값을 반환하고, 만약 좌항의 값이 null 이라면 우항의 값을 반환한다. 명심해야할점은 좌항이 null인 경우에만 우항의 값이 적용된다는 것이다.
코틀린에서 throw와 return 표현식은 엘비스 연산자의 우항으로 사용하는게 가능하다. 예를들어서 아래와같이 함수의 매개변수에서 그렇다.
fun foo(node : Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllgalArgumentException("name expected")
}

The !! operator

!! 연산자는 NullPointException 애호가들을 위한 것이다. null이 아닌 연산자 !!는 어떤 값이든 null이 아닌 타입으로 변환시켜주고 만약 값이 null인 경우 예외를 던져준다.
예로 변수 b를 b!!로 적을 수 있다. 그리고 이는 null이 아닌 객체 b를 반환해준다.
val l = b!!.length
그러므로 만약 NullPointException을 원한다면 그렇게 할 수 있따. 하지만 명시적으로 요청해야 하고 갑자기 나타나지 않는다.
 

Safe casts

객체가 목표한 타입이 아니라면 통상적인 캐스팅은 ClassCaseException 예외를 발생 시킨다.
다른 선택 사항은 null을 반환해주는 안전한 캐스팅을 해주는 것이다.
val aInt : Int? = a as? Int

Collections of a nullable type

collection의 null이 가능한 타입
null이 가능한 유형의 요소 컬렉션이 있고 null을 할 수 없는(null이 아닌) 요소를 필터링하려는 경우 filterNotNull을 사용하면 됩니다.
 
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()