|

코틀린 null safety 정리: ?. !! ?: 를 언제 써야 덜 위험할까

코틀린 null safety 정리: ?. !! ?: 를 언제 써야 덜 위험할까
코틀린 null safety는 null을 없애는 문법이 아니라, null 가능성을 타입과 흐름 안에서 드러내는 장치입니다.

코틀린 null safety는 Kotlin을 쓰는 이유 중 하나지만, 처음에는 `?.`, `!!`, `?:`가 비슷해 보입니다. 문법을 외우는 것보다 중요한 것은 null 가능성을 어느 지점에서 인정하고, 어느 지점에서 제거할지 정하는 기준입니다.

핵심은 null을 숨기지 말고 타입과 코드 흐름에 드러내는 것입니다. 이 글은 Kotlin 공식 null safety 문서를 기준으로 nullable type, safe call, Elvis operator, not-null assertion을 Android 실무 예시와 함께 정리합니다.

코틀린 null safety 핵심 판단 카드
safe call, Elvis operator, not-null assertion은 각각 쓰임새가 다릅니다.

코틀린 null safety의 출발점

Kotlin에서 `String`과 `String?`는 같은 타입이 아닙니다. `String`은 null이 들어올 수 없고, `String?`는 null이 들어올 수 있습니다. 이 차이가 컴파일러의 검사 기준이 됩니다.

Java에서는 null 가능성이 문서나 개발자의 기억에 남는 경우가 많습니다. Kotlin은 이 가능성을 타입에 직접 적게 합니다. 그래서 함수 인자, 반환값, 프로퍼티를 볼 때 null 가능성을 먼저 읽을 수 있습니다.

val name: String = "bscode"
val nickname: String? = null

println(name.length)      // 가능
println(nickname.length)  // 컴파일 오류

이 오류는 불편함이 아니라 안전장치입니다. 컴파일러가 ‘이 값은 null일 수 있으니 바로 멤버에 접근하지 말라’고 알려주는 것입니다.

safe call ?. 은 null을 통과시키는 문법

safe call `?.`은 왼쪽 값이 null이 아니면 오른쪽 멤버에 접근하고, null이면 전체 결과를 null로 만듭니다. null일 때 예외를 던지는 대신 결과 타입에 null 가능성을 남깁니다.

val user: User? = repository.findUser(id)
val email: String? = user?.email

이 코드는 user가 없을 수 있다는 사실을 숨기지 않습니다. `email`도 null일 수 있음을 타입으로 이어받습니다. 그래서 다음 단계에서 기본값을 줄지, 화면을 중단할지, 에러로 처리할지를 다시 결정해야 합니다.

Elvis operator ?: 는 null일 때의 결정을 적는 문법

Elvis operator `?:`는 왼쪽 값이 null이면 오른쪽 값을 사용합니다. 단순 기본값뿐 아니라 `return`, `throw`와 함께 쓰면 null을 함수 초반에서 제거할 수 있습니다.

fun render(user: User?) {
    val name = user?.name ?: "이름 없음"
    println(name)
}

fun requireUser(user: User?): User {
    return user ?: throw IllegalArgumentException("user가 필요합니다")
}

첫 번째 예시는 화면 표시용 기본값에 가깝고, 두 번째 예시는 함수가 계속 진행하려면 반드시 user가 필요하다는 계약에 가깝습니다. 같은 `?:`라도 의도가 다릅니다.

!! 는 null 가능성을 강제로 무시한다

not-null assertion `!!`는 nullable 값을 non-null처럼 취급합니다. 값이 null이면 `NullPointerException`이 발생합니다. 그래서 `!!`는 null safety를 강화하는 문법이라기보다, 컴파일러의 경고를 개발자가 책임지고 넘기는 문법에 가깝습니다.

val user: User? = repository.findUser(id)
println(user!!.name) // user가 null이면 런타임 예외

!!를 쓰기 전에 null이 왜 불가능한지 코드로 설명할 수 있어야 합니다. 설명이 어렵다면 `?: return`, `?: throw`, `if (user == null)` 같은 흐름 제어가 더 안전한 경우가 많습니다.

Android 코드에서 자주 만나는 판단 기준

Android에서는 lifecycle, intent extra, saved state, API 응답, view binding 해제 지점에서 nullable 값이 자주 등장합니다. 이때 모든 값을 억지로 `!!`로 바꾸면 null safety의 장점이 사라집니다.

  • 화면에 없어도 되는 값이면 `?.`와 기본 UI 상태를 사용한다
  • 없으면 화면을 진행할 수 없는 값이면 초반에 `?: return` 또는 `?: throw`로 끊는다
  • 외부 입력은 nullable로 받고 내부 도메인 경계에서 검증해 non-null로 바꾼다
  • 테스트나 초기화 순서 때문에 잠깐 비어 있는 값은 `lateinit`이나 `lazy`가 더 적절한지 따로 본다

let과 함께 쓸 때의 장점과 함정

nullable 값을 안전하게 열어야 할 때 `?.let { }`을 자주 씁니다. null이 아니면 블록이 실행되고, 블록 안에서는 값이 non-null로 다뤄집니다.

user?.let { foundUser ->
    analytics.logUser(foundUser.id)
    renderName(foundUser.name)
}

하지만 모든 null 처리를 `let`으로 감싸면 코드가 오른쪽으로 깊어집니다. 단순히 없으면 중단해야 하는 값은 초반 return이 더 읽기 쉽습니다.

실무 체크리스트

  1. 값이 정말 없을 수 있는지 먼저 확인한다
  2. 없어도 되는 값이면 `?.`로 흐름을 이어간다
  3. 기본값이 자연스러우면 `?:`로 명시한다
  4. 없으면 진행할 수 없으면 함수 초반에서 `?: return` 또는 `?: throw`로 끊는다
  5. `!!`는 외부 API나 초기화 순서가 확실한 좁은 구간에서만 쓴다

정리

코틀린 null safety는 null을 마법처럼 없애지 않습니다. 대신 null 가능성을 타입에 드러내고, 개발자가 안전한 흐름을 선택하게 만듭니다.

함수 흐름을 더 깊게 보고 싶다면 코틀린 Nothing 타입이란 무엇인가, 코틀린 let, run, apply, also, with 차이, 코틀린과 자바를 함께 쓸 때 클린코드가 깨지는 이유도 함께 보면 좋습니다. 외부 기준은 Kotlin Docs – Null safety, Kotlin Docs – Basic types, Android Developers – Kotlin style guide를 확인했습니다.

함께보면 좋은 글