동적 언어로 오랜 기간 개발을 해온 저는 Null
이 가져올 수 있는 후폭풍을 깊게 고민해본 적이 없었던 것 같습니다. Python에서의 None
, Ruby에서의 nil
은 큰 문제라기보다는, "비어있구나.."
또는 "아, nil이 올수도 있네 대응해야겠다."
하고 주제 넘게 가볍게 여겼습니다.
하지만 코틀린은 기본적으로 Null
이 변수에 담길 수 없게 제한하고 있을 정도로 Null
에 대해 심각하게 다룹니다. Java를 기반으로 둔 코틀린은 Java에서 문제로 여겨졌던 NPE(NullPointerException)을 대응하고 나온 언어입니다.
Null
레퍼런스가 Billion Dollar Mistake라고 표현하는 경우도 많습니다. 1965년도에 처음으로 공개된Null
은 퀵소트를 창시한 Tony Hoare에 의해서 만들어졌는데, 당시에는 단순하게 구현하기 편하기 위해서 만들어졌습니다. 하지만 현대에 와서는 사람들이 역사적으로 잘못된 발명이라고들 합니다.
앞서 말했듯이 코틀린에서는 기본적으로 Null
이 변수에 담기지 못하게 제한한다고 했습니다.
물론 담게 하는 방법이 존재하지만 코틀린은 그만큼 NPE와 치열한 싸움을 하고 있습니다. ⚔️
null
을 변수에 담으려고하면 아래와 같은 현상이 발생합니다.
val catName: String = null
// Null can not be a value of a non-null type String
Intellij IDE에서 Null
이 non-null
타입의 String
에 할당할 수 없다고 말해주며 빨간줄이 생깁니다.
하지만 Null
이 필요한 경우는 자주 생깁니다.그럴 경우에는 (?)를 붙여줘서 Null
을 담을 수 있게 하면 됩니다.
val catName: String? = null
println(catName) //null
아주 쉽죠!! 😎
코틀린이 Null
을 제한한다고 하긴 하지만 생각보다 쉽게 할당할 수 있습니다.
그치만 Null
이 할당된 변수를 다룰 때는 기존과는 다른 연산자들을 사용해야합니다. 기초적인 +
/
*
와 같은 사칙연산자들도 Nullable
한 변수에는 사용할 수 없습니다.
val cats: Int? = 1
println(cats + 1)
//Operator call corresponds to a dot-qualified call 'cats.plus(1)' which is not allowed on a nullable receiver 'cats'.
(?)을 타입 이름 뒤에 붙여줌으로써 Nullable referance
가 된 변수에는 뭐가 담겨져 있을지 모르기 때문에 연산을 하지 못하게 제한합니다.
코틀린은 Null referance
로 인해서 발생할 수 있는 문제들을 원천봉쇄하기 위해 타입 시스템을 설계한 언어입니다.
그래도 너무 많이 제한하는게 아닌가 싶겠지만 Null
을 안전하게 사용하기 위해 Safe Call이라는 개념이 등장합니다.
Nullable
한 변수에 접근하기 위한 방법으로 safe call operator
인 ?.
을 사용할 수 있습니다.
앞서 선언했던 예시에 한번 대입해보겠습니다.
val catName: String? = null
println(catName.length)
//Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
println(catName?.length)
//null
문자열의 길이를 가진 프로퍼티인 length
를 불러오면 safe or non-null asserted calls에만 허용한다고 제한합니다.
그래서 catName
뒤에 ?
를 붙여줘서 safe call
로 만들어주면 null
이 출력되는 것을 확인 할 수 있습니다.
Safe call을 non-nullable
한 변수에도 붙여줄 수 있지만 불필요한 Safe call이라고 말해줍니다.
val catName: String = "Fluffy"
println(catName?.length)
//Unnecessary safe call on a non-null receiver of type String.
//output => 6
Unnecessary safe call on a non-null receiver라는 문구가 뜨면서 IDE에서 dot call
로 변환하길 추천합니다.
위에서 실패했던 사칙연산을 다시 봅시다.
println(cats? + 1)
//온갖 에러
위와 같은 형태는 당연히 오류가 납니다. 😞
Nullable
한 변수로 연산을 할때는 .plus()
, .minus()
, .rem()
와 같은 메소드들을 사용해야합니다.
println(cats?.plus(1))
// 2
println(cats?.times(8))
// 8
코틀린에서 Null safety
를 말하면 Elvis operator
를 빼놓을 수 없습니다.
Elvis Operator는 ?:
로 생김새가 록앤롤의 제왕 Elvis presley
를 닮아 붙여진 이름입니다.
Null
을 다루다보면 꼭 나오면 상황이 있습니다.
x
값이Null
일 때는 0을 저장하고 아니면x
값을 저장하게 하자!
이런 상황에서 if
문으로 null
인지 체크하고 분기를 태움으로 문제를 해결 할 수 있습니다. 삼항연산자
하지만 코틀린에서는 이것을 간소화할 수 있습니다.
val dogName: String? = "Sulgi"
val nameLength: Int = if (dogName! = null) dogName.length else 0
----------------
val nameLength: Int = dogName?.length?:0
위의 두 코드는 같은 일을 하지만 Elvis operator
를 사용했을 때 쉽고 명확하게 이해할 수 있습니다.
이름이 Bang Bang
이라는 연산자가 있다니...
Bang Bang Operator
(!!)는 NPE를 사랑하는 사람들을 위해 만들어진 연산자입니다.
NPE를 코틀린이 아닌 우리가 다루겠다면 사용할 것을 권하는 연산자라고 말하는 사람들도 있습니다.
val length = dogName!!.length
위와 같이 Nullable
한 변수에 (!!)을 붙이게 되면 변수에 들어 있는게 어떤 값이든 non-null
타입으로 변환해줍니다.
하지만 non-null
타입으로 변환하려는 변수가 null
이면 NPE가 발생하게 되는거죠.😰
Data class
에서 Safe call이 어떻게 사용되는지 예제로 보겠습니다. Data class
두개를 정의 해봅시다.
data class Dog(val species: Species?)
data class Species(val name: String?)
species
와 name
필드는 nullable
한 타입들입니다. 두 필드를 안전하게 접근하기 위해서는 Safe call
을 이용할 수 있습니다.
val dog: Dog? = Dog(Species("Maltese"))
val result = dog?.species?.name
assertEquals(result,"Maltese")
만약 dog
의 species
가 null
을 가지고 있다면,
val dog: Dog? = Dog(Species(null))
val result = dog?.species?.name
assertNull(result)
위와 같이 사용할 수 있습니다.