null은 값이 부족하다는 것을 나타낸다.
-
프로퍼티가 null인 경우
값이 설정되지 않았거나 제거되었음
-
null을 리턴하는 경우 여러 의미를 가질 수 있음
- String.toIntOrNull()은 String을 Int로 적절히 변환할 수 없을 때 null
- Interable
.firstOrNull()은 주어진 조건에 맞는 요소가 없는 경우 null
이러한 null은 nullable 값의 처리를 위해 명확한 의미를 갖는 것이 좋으며 아래와 같은 세가지 방법으로 처리한다.
- safe call(?), smart casting, elvis operator 연산자를 활용해 안전하게 처리한다.
- 오류를 throw 한다.
- 함수 또는 프로퍼티를 리팩토링해서 nullable 타입이 나오지 않게 바꾼다.
null을 안전하게 처리하기
-
safe call, smart casting
널리 사용되는 방법으로는 safe call과 smart casting이 있다.
printer?.print() // safe call
if (printer != null) printer.print() // smart casting
-
elvis operator
elvis operator 또한 많이 사용되며 elvis operator 오른쪽에는 return, throw를 포함한 모든 표현식이 허용된다.
이는 return, throw 모두 Nothing을 리턴하게 설계되어서 가능하다.
val printerName = printer?.name ?: "Unnamed"
val printerName = printer?.name ?: return
val printerName = printer?.name ?: throw Error("error")
- smart casting with contracts feature(규약 기능)
val name = readLine()
if (!name.isNullOrBlank()) {
println(name.toUpperCase()) // smart casting에 의해 safe call(?) 없이 toUpperCase 호출 가능
}
방어적 프로그래밍과 공격적 프로그래밍
- 방어적 프로그래밍(defensive programming)
모든 가능성을 올바른 방식으로 처리하는 것 (null일때 출력하지 않기 등)
- 공격적 프로그래밍(offensive programming)
예상하지 못한 상황 발생 시 개발자에게 알려서 수정하게 만드는 방식 require, check 등이 공격적 프로그래밍을 위한 도구이다.
오류 throw 하기
아래와 같은 상황에서 names가 null이면 별도의 행위 없이 코드가 이어서 진행된다.
val names: List<String>? = getNames()
if (!new.isNullOrEmpty()) {
names.forEach { println(it) }
}
만약 names가 null이 될 것을 예상하지 못한다면 println이 호출되지 않아 의아할 것이며 개발자가 오류를 찾기 어렵게 만든다.
이러한 부분에서 문제가 발생할 경우 throw, !!, requireNotNull, checkNotNull등을 활용해 개발자에게 오류를 강제로 발생시켜 주는 것이 좋다.
not-null assertion(!!)과 관련된 문제
!!은 nullable이지만 null이 나오지 않는다는 것이 확실한 상황에 많이 사용된다. not-null assertion(!!)은 사용하기 쉽지만 좋은 해결 방법은 아니다.
현재 null이 나오지 않는 사실이 확실하다고 미래에 확실한 것은 아니기 때문이다.
class ABC {
private var controller: UserController? = null
fun init() {
controller = UserController()
}
fun test() {
controller!!.doSomething()
}
}
위와 같이 사용하면 controller!!.doSomething()
와 같이 프로퍼티를 계속 언팩(unpack)해야 하므로 사용하기 귀찮다.
이 경우 lateinit 또는 Delegrates.notNull을 활용하는게 좋다.
의미 없는 nullablity 피하기
nullability는 어떻게든 처리해야 하므로 추가 비용이 발생한다.
따라서 필요한 경우가 아니라면 nullability 자체를 피하는게 좋다.
- 클래스에서 nullability에 따라 List
의 get 혹은 getOrNull과 유사한 여러 함수를 만들어 제공한다. - 클래스 생성 이후 확실히 생성된다는 보장이 있으면 lateinit 프로퍼티 혹은 notNull 델리게이트 활용
- 빈 컬렉션 대신 null을 리턴하지 말자, 요소가 부족한 것을 나타낼때는 empty 컬렉션 활용
- nullable enum과 None enum 값은 완전히 다른 의미이다. null인 사용하는 곳에서 별도로 처리하지만, None enum은 정의에 없으므로 필요한 경우 추가해서 활용할 수 있다는 의미
lateinit 프로퍼티와 notNull 델리게이트
클래스 생성 중 초기화 할 수 없는 프로퍼티를 가지는 것은 드문 일은 아니지만 분명 존재하는 일이다.
이러한 프로퍼티는 사용전 반드시 초기화가 필요하다.
class ABC {
private var controller: UserController? = null
fun init() {
controller = UserController()
}
fun test() {
controller!!.doSomething()
}
}
매번 !! operator를 활용해 nullable에서 타입 변환해 사용하는 것은 바람직하지 않다.
이때는 아래처럼 lateinit 혹은 notNull Delegates를 활용하자.
class ABC {
private lateinit var controller: UserController
fun init() {
controller = UserController()
}
fun test() {
controller!!.doSomething()
}
}
lateinit은 nullable과 비교해 아래와 같은 이점이 있다.
- !! operator를 이용한 언팩이 불필요하다.
- 이후에 null로 사용하고 싶을 때 nullable로 만들수도 있다.
- 프로퍼티가 초기화된 이후에는 초기화되지 않은 상태로 돌아갈 수 없다.
아래처럼 onCreate때 초기화 해 lateinit을 사용할 수 없는 경우 Delegrates.notNull을 사용한다.
private var doctorId: Int by Delegates.notNull()
fun onCreate() {
docterId = // 생략
}
다음과 같이 property delegation을 활용할 수도 있다.
private var doctorId: Int by arg(DOCTER_ID_ARG)
Reference
개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.
오류가 있다면 댓글을 남겨주세요.