안드로이드 코틀린 언어 학습 노트 목차

널 안전 — ?·!!·Elvis·플랫폼 타입·스마트 캐스트

토니 호어는 null 참조를 *"10억 달러짜리 실수"*라 불렀다 — 자바 앱이 죽는 가장 흔한 원인이 NullPointerException(NPE)이다. 코틀린의 가장 큰 한 걸음null을 타입 시스템에 넣은 것이다. 어떤 변수가 null이 될 수 있는지를 타입으로 구분하고, 컴파일러가 null 처리를 강제한다. 그래서 대부분의 NPE를 컴파일 타임에 잡는다. 이게 안드로이드에서 코틀린이 자바를 밀어낸 핵심 이유다. (출처: Kotlin Docs — Null Safety / Kotlin in Action 2e Ch 6.)

널 가능 타입 — 기본은 not-null

코틀린에서 보통의 타입은 null을 담을 수 없다. null을 허용하려면 *타입 뒤에 ?*를 붙인다.

var a: String = "hi"
// a = null          // ❌ 컴파일 에러: String은 null 불가

var b: String? = "hi"
b = null             // ✅ String? 은 null 허용

val len = a.length   // ✅ a는 not-null이라 바로 접근
// val len2 = b.length  // ❌ 컴파일 에러: b는 null일 수 있어 그냥 접근 금지

핵심 전환 — "이 값이 null일 수 있나?"를 타입이 말해준다. String을 받는 함수엔 절대 null이 안 들어오고, String?을 받으면 null을 처리해야 한다. 의도가 시그니처에 박힌다.

안전 호출 ?. — null이면 통째로 null

널 가능 값에 접근하는 가장 흔한 방법이 안전 호출(?.)수신자가 null이면 호출을 건너뛰고 전체 식이 null이 된다.

val len: Int? = b?.length          // b가 null이면 len도 null (NPE 없음)
// 체이닝 — 중간 어디든 null이면 전체가 null
val city: String? = user?.address?.city
다이어그램 로딩 중…

Elvis ?: — null일 때 기본값

null이면 대체값을 주고 싶을 때 **Elvis 연산자(?:)**를 쓴다(?:를 옆으로 보면 엘비스 머리 모양).

val name: String = user?.name ?: "익명"        // null이면 "익명"
val len: Int = b?.length ?: 0                  // null이면 0

// 이른 반환·예외에도 — 좌변이 null이면 우변 실행
fun process(u: User?) {
    val user = u ?: return                     // null이면 함수 종료
    val id = u?.id ?: throw IllegalArgumentException("id 없음")
}

?. + ?: 조합이 코틀린 null 처리의 일상이다 — "안전하게 접근하고, null이면 대체."

not-null 단언 !! — 최후의 수단(코드 스멜)

**!!**는 *"이건 절대 null 아니야, 책임질게"*라고 컴파일러에게 단언한다 — null이면 그 자리에서 NPE를 던진다.

val len = b!!.length     // b가 null이면 즉시 NPE

!!코틀린의 null 안전을 스스로 끄는 것이라 남용은 코드 스멜이다. 정말 논리적으로 null이 불가능하다고 확신할 때만, 그것도 최소한으로 쓴다. 대부분은 ?.·?:·스마트 캐스트로 대체된다 — !!가 여기저기 보이면 설계를 의심하라.

스마트 캐스트 — 검사하면 컴파일러가 기억한다

코틀린은 null 검사·타입 검사를 하면, 그 블록 안에서 타입을 자동으로 좁혀준다스마트 캐스트.

fun printLen(s: String?) {
    if (s != null) {
        println(s.length)     // ✅ 여기선 s가 String으로 스마트 캐스트됨
    }
}

fun describe(x: Any) {
    if (x is String) {
        println(x.length)     // ✅ is 검사 후 String으로 캐스트 (자바의 instanceof+캐스트 불필요)
    }
}

단, 스마트 캐스트는 값이 안 바뀐다고 보장될 때만 된다 — var 프로퍼티(특히 다른 스레드가 바꿀 수 있는)엔 안 되므로, 지역 val에 담아 검사하는 패턴을 쓴다.

안전 캐스트 as? — 실패하면 null

타입 캐스트가 실패할 수 있으면 as?실패 시 예외 대신 null.

val str = obj as? String          // obj가 String 아니면 null (ClassCastException 없음)
val len = (obj as? String)?.length ?: 0

let — null 아닐 때만 블록 실행

?.let { }null이 아닐 때만 블록을 실행하는 관용구다([스코프 함수] 장에서 자세히).

user?.let {
    // user가 null이 아닐 때만 여기 실행, it == user (non-null)
    println(it.name)
    save(it)
}

플랫폼 타입 — 자바와 만나는 위험 지대

자바 코드엔 null 정보가 없다(어노테이션 없으면). 코틀린은 자바에서 온 타입을 **플랫폼 타입(String!)**으로 보는데 — null 검사를 강제하지 않는 회색 지대다. 그래서 자바가 null을 돌려주면 런타임 NPE가 날 수 있다.

// 자바: public String getName() { return null; }
val name = javaObj.name       // 타입은 String! (플랫폼) — 검사 안 해도 컴파일됨
println(name.length)          // 💥 자바가 null 주면 런타임 NPE

방어법 — 자바 쪽에 *@Nullable/@NonNull(JSR-305·androidx.annotation)*을 달면 코틀린이 String?/String으로 정확히 본다. 자바 경계에서 온 값은 받자마자 String?로 명시하고 null 처리하는 게 안전하다.

lateinit·lazy — null 없이 나중에 초기화

안드로이드에선 생성자에선 못 채우고 나중에(onCreate 등) 채우는 프로퍼티가 흔하다. String?로 두면 매번 ?.가 번거롭다 → **lateinit var**로 not-null이되 나중 초기화를 약속한다.

lateinit var binding: ActivityMainBinding   // not-null, 나중에 초기화
// onCreate에서: binding = ...
// 초기화 전 접근하면 UninitializedPropertyAccessException
if (::binding.isInitialized) { /* 초기화 여부 확인 가능 */ }

val config: Config by lazy { loadConfig() }  // 첫 접근 시 한 번 초기화 (불변)

정리 — null을 타입으로 다스린다

코틀린 null 안전의 흐름은 한 줄로 — 타입으로 null 가능성을 구분(?)하고, 안전 호출(?.)·Elvis(?:)·스마트 캐스트로 처리하며, !!는 최소화하고, 자바 경계(플랫폼 타입)를 조심한다. 이 습관이 NPE를 컴파일 타임으로 끌어와 앱을 안정시킨다.

한 줄 요약 — 기본 타입은 not-null, null 허용은 ?. 접근은 ?.(null이면 null)·?:(기본값)·as?(안전 캐스트)·?.let{}. !!는 스멜(NPE 위험). is/null 검사 후 스마트 캐스트. 자바에서 온 *플랫폼 타입(T!)*은 NPE 위험 지대 — 어노테이션·명시 타입으로 방어. lateinit/lazy로 나중 초기화.

(출처: Kotlin Docs — Null Safety / Kotlin in Action 2e Ch 6 / Android — Kotlin & null safety.)

코틀린 기초 — val/var·타입추론·함수·표현식클래스·객체 — data·sealed·enum·object·생성자