반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item8. 적절하게 null을 처리하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item8. 적절하게 null을 처리하라

어리둥절범고래 2023. 9. 9. 15:19
반응형

프로퍼티가 null이라는 것은 값이 설정되지 않았거나, 제거되었다는 것을 나타냅니다.

함수가 null을 리턴한다는 것은 함수에 따라서 여러 의미를 가질 수 있습니다.

 

  • String.toIntOrNull()은 String을 Int로 적절하게 변환할 수 없을 경우 null을 리턴한다.
  • Iterable<T>.firstOrNull(() -> Boolean)은 주어진 조건에 맞는 요소가 없을 경우 null을 리턴한다.

 

기본적으로 nullable 타입은 세 가지 방법으로 처리합니다.

 

  • ?. , 스마트 캐스팅, Elvis 연산자 등을 활용해서 안전하게 처리한다.
  • 오류를 throw한다.
  • 함수 또는 프로퍼티를 리펙터링해서 nullable 타입이 나오지 않게 바꾼다.

 

 


null을 안전하게 처리하기

 

 

null을 안전하게 처리하는 방법 중 널리 사용되는 방법으로는 safe call과 스마트 캐스팅이 있습니다.

 

printer?.print() // safe call
if (printer != null) printer.print() // 스마트 캐스팅

 

다른 방법으로는 Elvis 연산자를 사용하는 것입니다.

Elvis 연산자는 오른쪽에 return 또는 throw을 포함한 모든 표현식이 허용됩니다.

 

val printerName1 = printer?.name ?: "Unnamed"
val printerName2 = printer?.name ?: return
val printerName3 = printer?.name ?: throw Error("Printer must be named")

 

 

스마트 캐스팅은 코틀린의 규약 기능(contracts feature)을 지원합니다.

이 기능을 사용하면 다음 코드처럼 스마트 캐스팅할 수 있습니다.

 

println("What is your name?")
val name = readLine()
if (!name.isNullOrBlank()) {
    println("Hello ${name.toUpperCase()}")
}

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

 

 


오류 throw하기

 

 

이전 printer 예제에서 printer가 null이 되리라 예상하지 못했다면, print 메소드가 호출되지 않아서 이상할 것입니다.

이는 개발자가 오류를 찾기 어렵게 만듭니다.

그러한 부분에서 오류를 강제로 발생시켜 개발자에게 알려주는 것이 좋습니다.

오류를 강제로 발생시킬 때는 throw, !!, requireNotNull, checkNotNull 등을 활용합니다.

 

fun process(user: User) {
    requireNotNull(user.name)
    val context = checkNotNull(context)
    val networkService = getNetworkService(context) ?: throw NoInternetConnection()
    networkService.getData { data, userData ->
        showFor(data!!, userData!!)
    }
}

 

 


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

 

 

nullable을 처리하는 가장 간단한 방법은 not-null assertion(!!)을 사용하는 것입니다.

어떤 대상이 null이 아니라고 사용하면 NPE 예외가 발생합니다.

가장 간단한 방법이지만, 예외가 발생할 때 어떤 설명도 없는 제네릭 예외가 발생하기 때문에 좋은 방법이 아닙니다.
아래와 같은 경우 nullability(null일 수 있는지)와 관련된 정보는 숨겨지게 되므로 놓칠 수 있습니다.

 

fun largestOf(vararg nums: Int): Int = nums.max()!!
largestOf() // NPE

 

일반적으로 !! 연산자 사용을 피해야 합니다.

대부분의 팀이 !! 연산자를 아예 사용하지 못하게 하는 정책을 갖고 있습니다.

(Detekt와 같은 정적 분석 도구는 !! 연산자를 사용하면, 아예 오류를 발생하도록 설정되어 있습니다)

 


의미 없는 nullability 피하기

 

 

nullability는 어떻게든 적절하게 처리해야 하므로 필요한 경우가 아니라면, nullability 자체를 피하는 것이 좋습니다.

nullability를 피할 대 사용할 수 있는 몇 가지 방법입니다.

 

  • 클래스에서 nullability에 따라 여러 함수를 만들어서 제공할 수도 있다. 대표적인 예로 List<T>의 get과 getOrNull 함수가 있다.
  • 어떤 값이 클래스 생성 이후에 확실하게 설정된다는 보장이 있다면, lateinit 프로퍼티와 notNull 델리게이트를 사용하라.
  • 빈 컬렉션 대신 null을 리턴하지 말라. null은 컬렉션 자체가 없다는 것을 나타낸다. 요소가 부족하다는 것을 나타내려면, 빈 컬렉션을 사용하라.
  • nullable enum 대신 None enum으로 처리하라. nullable enum과 None enum 값은 완전히 다른 의미이다. null enum은 별도로 처리해야 하지만, None enum 정의에 없으므로 필요한 경우에 사용하는 쪽에서 추가해서 활용할 수 있다는 의미이다.

 

 


lateinit 프로퍼티와 notNull 델리게이트

 

 

클래스가 클래스 생성 중에 초기화할 수 없는 프로퍼티를 가질 수 있으며, 이러한 프로퍼티는 사용 전에 반드시 초기화해서 사용해야 합니다.

이러한 코드에 대한 바람직한 해결책은 나중에 속성을 초기화할 수 있는, lateinit 한정자를 사용하는 것입니다.

초기화 전에 값을 사용하려고 하면 예외가 발생하며, 처음 사용하기 전에 반드시 초기화될 거라고 예상되는 경우에만 사용해야 합니다.

 

lateinitr은 nullable과 비교해서 다음과 같은 차이가 있습니다.

 

  • !! 연산자로 언팩(unpack)하지 않아도 된다.
  • 이후에 어떤 의미를 나타내기 위해 null을 사용하고 싶을 때, nullable로 만들 수 있다.
  • 프로퍼티가 초기화된 이후에는 초기화되지 않은 상태로 돌아갈 수 없다.

 

lateinit을 사용할 수 없는 경우도 있습니다.

기본(Primitive) 타입은 lateinit으로 선언할 수 없으며,

Int, Long, Double, Boolean과 같은 기본 타입과 연결된 타입으로 프로퍼티를 초기화해야 하는 경우는 lateinit을 사용할 수 없습니다.

이런 경우에는 Delegates, notNull을 사용하며, 프로퍼티 위임(property delegation)을 사용할 수도 있습니다.

 

private lateinit var doctorId: Int
// 'lateinit' modifier is not allowed on local variables of primitive types
class DoctorActivity : Activity() {
    private var doctorId: Int by Delegates.notNull()
    private var fromNotification: Boolean by arg(FROM_NOTIFICATIOM_ARG)
}
반응형