006 Kotlin Nullsafe

tech blog 글 읽고 정리하기 #

[kotlin] 널 세이프 프로그래밍 알아보기 #

Null Safe 프로그래밍 #

  • kotlin은 기본적으로 변수에 null을 할당할 수 없도록 제약하고 있다.
  • kotlin에서 NullPointerException (NPE) 가 발생할 수 있는 원인은 다음과 같다.
    • 명시적으로 throw NullPointerException() 을 수행한경우
    • ‘!!’ 오퍼레이터를 사용한경우
    • 다음과 같은 경우 초기화와 관련된 데이터 불일치가 발생한다.
      • 생성자에서 사용할 수 있는 초기화 되지 않은 this가 전달되어 사용된경우
      • 수퍼클래스 생성자가 오픈 멤버를 호출한경우, 이때 오픈멤버가 초기화 되지 않은 객체인경우
    • 자바와 함께 사용하는 경우
      • 참조하는 Java 대상 객체가 널인경우
      • 자바 코드가 Generic 타입을 담는 객체에 널은 대입하고 이를 kotlin이 참조하는 경우

기본적으로 null을 변수에 담을 수 없다. #

  1. 다음 코드와 같이 변수에 null을 대입하는 경우 컴파일 오류가 난다.
// 일반적으로 변수에는 널을 할당할 수 없다. 
var a: String = "abc" 
a = null // compilation error 가 발생한다. 

Null을 담을 수 있도록 선언하기 #

  1. 다음과 같이 <타입>? 으로 지정한경우 널을 할당할 수 있다.
// 다음 변수에는 널을 설정할 수 있다. 
var b: String? = "abc"
b = null // 널을 할당할 수 있다. 
print(b)
  1. 샘플을 활용하면 다음과 같다.
fun main(args: Array<String>) {
  var a: String = "Hello1"
  var b: String? = "World"
  b = null
  print(b)

  //  다음은 NPE가 발생하지 않고 안전한 코드가 된다. 
  //  즉 val은 한번 값을 할당하면 다시 할당이 불가능하기 때문이다.
  val l = a.length

  //  다음은 널이 될 수없어서 오류가 난다. 
  //  01.NullTest.kt:13:13: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
  val l2 = b.length
}

조건 검사 시에 널을 체크 #

  1. 다음과 같이 b가 널인지 명시적으로 확인하고 두 옵션을 별도로 처리할 수 있다.
val l = if (b != null) b.length else -1
  1. 다음은 널체크를 수행하는 방법을 보여준다.
val b: String? = "Kotlin"
if (b != null && b.length > 0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}

중요한 것중에서 val로 값을 할당하면 널에 대해서 안전하게 처리할 수 있다. (즉, 한번할당하면 값을 재 할당할 수 없으므로 널에 안전하게 처리할 수 있다.)

널에 대해서 안전하게 호출하는 방법 #

  1. ‘?.’ 을 이용하면 값이 널이라도 안전하게 처리할 수 있다. 이 의미는 널이 아니면 해당 값에 대한 참조를 수행하고, 널인경우라면 결과로 널을 반환한다.
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Unnecessary safe call

즉 위 코드는 b가 널인경우 null을 반환한다. 결과는 다음과 같다.

null
6
  1. 다음과 같이 ‘?.’ 은 체인으로 호출이 가능하다.
bob?.department?.head?.name
  1. 다음과 같이 리스트에 대해서 널에 대해서 안전하게 작업을 수행할 수 있다.
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let { println(it) } // prints Kotlin and ignores null
}

결과

Kotlin

Nullable Receiver 이용하기 #

  • 확장 함수는 nullable 수신가에서 정의될 수 있다.
  • 이렇게 하면 각 호출 사이트에서 null 검사 논리를 사용할 필요 없이 null에 대한 동작을 수행할 수 있다.
  • toString() 함수는 nullable receiver로 구현되어 있다.
  • 널이 전달되면 이는 “null” 을 출력한다.
val person: Person? = null
logger.debug(person.toString()) // Logs "null", does not throw an exception
  • 위와 같이 person.toString() 인경우 person이 널이라도 NPE가 일어나지 않는다.
  • 안전하게 호출한다면 다음과 같이 호출하자.
var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // Returns a String? object which is `null`
if (isoTimestamp == null) {
   // Handle the case where timestamp was `null`
}

엘비스 연산자 (?:) 이용하기 #

  • 널이 아닌경우 정상 처리를 수행하고, 널인경우 대안 처리를 하고자 한다면 엘비스 연산자를 사용하자.
val l: Int = if (b != null) b.length else -1
  • 위 내용을 엘비스 연산자로 구현하면 다음과 같다.
val l = b?.length ?: -1
  • 대안 작업에 예외도 던질 수 있다.
fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    // ...
}

위와 같이 node.getName()가 널인경우 IllegalArgumentException 이 발생한다.

‘!!’ 오퍼레이터 #

  • ‘!!’ 은 널이아닌지 검사하고, 널이 아닌경우 원래 값을 반환하고, 널인경우 NPE를 발생시킨다.
val l = b!!.length

만약 NPE를 원한다면 위와 같이 명시적으로 예외를 던질 수 있다.

안전하게 타입 캐스팅하기 #

  • 값을 다른값으로 타입 캐스팅을 할때 값이 널이면 ClassCastException이 발생한다.
  • 특정 타입에서 다른 타입으로 안전하게 캐스팅 하기 위해서는 다음과 같이 안전한 타입 캐스팅을 할 수 있다.
val aInt: Int? = a as? Int

위와 같이 a 값을 안전하게 Int 로 캐스팅이 가능하다.

컬렉션에서 널이 아닌 값만 추출하기 #

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

위와 같이 리스트의 값중에 널이 있는경우 filterNotNull() 함수를 이용하면 널이 아닌 값만 추출할 수 있다.