아이템 8 - 적절하게 null을 처리하라

ds_chanin

·

2022. 3. 3. 00:14


null은 ‘값이 부족하다’를 나타내며 상황에 따라 다음을 의미한다.

  • 값이 설정되지 않았다.
  • 값이 제거되었다.

따라서 null은 명확한 의미를 가지도록 하는 것이 좋다.

nullable 타입을 다루는 방법

다음 세가지 방법으로 다룬다.

  • 안전호출(?.), 스마트 캐스팅, 엘비스(Elvis ?:) 연산자
  • 오류를 throw
  • 리팩토링을 통해 nullable 제거

null을 안전하게 처리하기

아래 두가지 방법은 애플리케이션 사용자 관점에서 가장 안전한 방법이다.

안전호출을 사용하기

val printer: Printer? = getPrinter()
printer?.print()

val printerName1 = printer?.name ?: "Unnamed"
val printerName2 = printer?.name ?: return
val printerName3 = printer?.name ?: throw RuntimeException("프린터 이름 없음")

스마트 캐스트를 사용하기

val printer: Printer? = getPrinter()
if(printer != null) {
    printer.print()
}

val name = readLine()
if (!name.isNullOrBlank()) {
    println("이름 : $name")
}

val news: List<News>? = getNews()
if(!news.isNullOrEmpty()) {
    news.foreach { notify(it) }
}

null 처리시 방어적 프로그래밍과 공격적 프로그래밍

방어적 프로그래밍: 모든 상황을 처리할 수 있어 올바른 방식으로 처리하는 것

공격적 프로그래밍: 모든 상황을 처리하는 것이 불가능하여 문제를 개발자에게 알려서 수정하도록 하는 것

ex) 충전후 충전으로 발생한 데이터를 이벤트로 발행해야 하는 상황

방어적 프로그래밍

fun charge() {
    val chargeHistory: ChargeHistory? = chargeRequest()
    chargeHistory?.run {
        send(this)
    }
}

공격적 프로그래밍

fun charge() {
    val chargeHistory: ChargeHistory = chargeRequest() ?: throw IllegarArgumentException("충전 자료 없음")
	send(chargeHistory)
}

오류를 throw

다른 개발자가 ‘당연히 그럴 것이다'라고 생각하게 되는 부분이 있고, 그 부분에서 문제가 발생한다면 오류를 강제로 발생시켜 주는 것이 좋다.

throw, !!, requireNotNull, checkNotNull 을 활용하도록 하라.

not-null assertion(!!)과 관련된 문제

!! 를 사용하는 것은 좋은 해결 방법이 아니다. 일반적으로 !! 연산자 사용을 피하라.

제네릭 예외가 발생할 수 있고, !! 를 사용하면 자바의 nullable을 처리할 때 발생하는 문제가 똑같이 발생한다.

계속해서 언팩(unpack)해야하는 문제가 발생하고 의미있는 null 값을 가질 가능성을 차단해버리게 된다.

미래에 코드가 어떻게 변화할지 아무도 알 수 없다. 현재 null이 아니라고 확신할수 있어도 미래 확실한 것은 아니다.

!! 연산자가 의미 있는 경우는 굉장히 드물다. 자바 라이브러리를 사용하여 nullability가 제대로 표현되지 않는 경우에만 사용하는 것이 낫고, 코틀린 대상으로 설계된 API라면 의심하도록 하라.

의미 없는 nullability 피하기 (리팩토링)

필요한 경우가 아니라면, nullability 자체를 피하는 것이 좋다.

  • get, getOrNull 함수를 사용하도록 변경한다.
  • lateinit 프로퍼티, Delegates.notnull() 를 사용한다.
  • 컬렉션 반환시 null 리턴대신 빈 컬렉션을 사용한다.
  • 찾고자 하는 enum 이 없을때 사용자가 정의한 None enum을 사용할 것인지 nullable enum을 사용할지 명확히 고려하라

lateinit 프로퍼티와 Delegates.notnull()

프로퍼티를 사용할 때 마다 nullable에서 null이 아닌 것으로 타입을 변환하는 것은 위험하고 바람직하지 않다.

lateinit을 사용하는 경우 초기화 되지 않은 상태로 사용한다면 exception이 발생하기 때문에 라이프 사이클처럼 명확한 호출 순서를 가지고 있는 경우에 사용하는 것이 좋다. (ex. Junit @BeforeEach)

단, lateinit은 JVM에서 Int, Long, Double, Boolean과 같은 기본 타입을 초기화 할 때는 사용할 수 없다. 따라서 기본 타입 이외의 경우 Delegates.notNull() 을 사용하도록 한다.

class NullabilityProduct {
    lateinit var product: Product
    var price: Long by Delegates.notNull()
}