kyucumber
전체 글 보기

이펙티브 코틀린 아이템 08. 적절하게 null을 처리하라

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

개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.

오류가 있다면 댓글을 남겨주세요.