Lack of Value

null 은 값이 부족하다(lack of value) 는 것을 나타낸다.

함수가 null 을 리턴하는 것은 여러 의미를 가질 수 있으며, null 은 최대한 명확한 의미 를 가져야 한다. null 사용이 나쁜 것은 아닌데, 보기에 의미가 없는 null 일 경우에는 나쁜 케이스에 해당된다.

  • String.toIntOrNull() 은 Int 로 변환할 수 없을 경우 null 을 반환
  • Iterable.firstOrNull(() -> Boolean) 은 주어진 조건에 맞는 요소가 없을 경우 null 을 반환

Handling Nulls Safely

Safe Call & Smart Casting

printer?.print() // safe call
if (printer != null) printer.print() // smart casting
val printName = printer?.name ?: "Unnamed"

Smart Casting 은 코틀린의 규약 기능(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) }
}

Exception Throwing

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

Notnull Assertion

nullable 을 처리하는 가장 간단한 방법은 Notnull Assertion(!!) 를 사용하는 것이다. 근데 사실 좋은 방법은 아니다. !! 기호가 되게 못생겼는데, 그 이유는 가급적 사용하지 말라는 뜻으로 설계되었다고 알고 있다.

!! 를 사용하면 자바에서 nullable 을 처리할 때 발생할 수 있는 문제가 똑같이 발생한다. 좋은 방법은 아니다.

!! 는 타입이 nullable 이지만 null 이 나오지 않는다는 것을 확신할 수 있는 경우 에만 사용해야 한다. 현재가 확실하다고 미래가 확실한 것은 아니다. !! 로 되어있다면 미래에 리팩토링할 때 다른 개발자가 nullability(널일 수 있는지) 를 놓치는 경우가 발생할 수 있다.

Avoiding Meaningless Nullability

의미 없는 nullability 는 피해야 한다. nullability 는 어떻게든 적절하게 처리해야 하므로 추가 비용이 발생한다.

How to avoiding meaningless nullability:

  • 함수 제공 (e.g List 의 get 과 getOrNull)
  • 어떤 값이 클래스 생성 이후에 확실하게 설정된다는 보장이 있다면, lateinit 프로퍼티와 notNull delegate 사용
  • 빈 컬렉션 대신 null 을 리턴하지 말기. null 은 컬렉션이 없다는 것을 의미한다. 따라서 요소가 부족하다는 것을 나타내려면 빈 컬렉션을 반환해야 한다.

lateinit property:

class UserControllerTest {
    private lateinit var repository: UserRepository
    private lateinit var controller: UserController
    
    @BeforeEach
    fun init() {
        repository = mockk()
        UserController(repository)
    }
    
    @Test
    fun test() {
        controller.doSomething()
    }
}

lateinit 은 처음 사용하기 전에 반드시 초기화가 되어있어야 한다. 프로퍼티가 초기화된 이후에는 초기화되지 않은 상태로 돌아갈 수 없다.

Delegates.notNull:

class DoctorActivity: Activity() {
    private var doctorId: Int by Delegates.notNull()
    private var fromNotification: Boolean by Delegates.notNull()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        doctorId = intent.extras.getInt(DOCTOR_ID_ARG)
        fromNotification = intent.extras.getBoolean(FROM_NOTIFICATION_ARG)
    }
}

lateinit 을 사용할 수 없는 경우도 있다. JVM 에서 Int, Long, Double, Boolean 과 같은 기본 타입과 연결된 타입으로 프로퍼티를 초기화해야 하는 경우이다. 이런 경우에는 lateinit 보다는 약간 느리지만, Delegates.notNull 을 사용한다.

아래와 같이 프로퍼티 위임(property delegation) 패턴을 사용하여 nullability 로 발생하는 여러 가지 문제를 안전하게 처리할 수 있다.

class DoctorActivity: Activity() {
    private var doctorId: Int by arg(DOCTOR_ID_ARG)
    private var fromNotification: Boolean by arg(FROM_NOTIFICATION_ARG)
}

References

  • Effective Kotlin / Marcin Moskala 저 / 인사이트