with ChatGPT 시리즈는 ChatGPT의 내용과 개인의 생각을 토대로 학습해 보는 컨텐츠입니다.
Kotlin은 NullPointerException(NPE)을 방지하기 위해 Null 안정성(Null Safety) 개념을 언어 차원에서 지원합니다.
자바에서는 null이 포함된 객체를 조작할 때 명시적인 체크 없이 접근하면 NullPointerException이 발생할 위험이 있지만, Kotlin은 다양한 연산자를 제공하여 이 문제를 방지할 수 있습니다.
이번 주제에서는 코틀린의 Null 안정성 개념과 다양한 Null 관련 연산자의 활용법을 살펴보겠습니다.
🔥 1. Null 안정성이란?
Kotlin에서 모든 변수는 기본적으로 null을 가질 수 없습니다.
즉, 아래 코드는 컴파일 오류가 발생합니다.
var name: String = "Kotlin"
name = null // ❌ 오류 발생 (Type mismatch: null cannot be a value of a non-null type String)
- String 타입은 기본적으로 null을 허용하지 않음
- 만약 null을 허용하려면 nullable 타입(?)을 명시적으로 지정해야 함
var name: String? = "Kotlin"
name = null // ✅ 가능
- String? 타입을 사용하면 null을 저장할 수 있음
🛠️ 2. Kotlin의 Null 관련 연산자
✅ 1️⃣ Safe Call (?.) - Null 체크 후 안전하게 호출
val length: Int? = name?.length
- name이 null이면 ?. 이후의 코드가 실행되지 않고 null을 반환
- NullPointerException을 방지하면서 안전한 호출이 가능
✅ 2️⃣ Elvis 연산자 (?:) - 기본값 제공
val name: String? = null
println(name?.uppercase()) // 출력: null (NPE 발생하지 않음)
💡 언제 사용하면 좋은가?
- 객체가 null일 가능성이 있을 때 안전하게 프로퍼티나 함수를 호출하고 싶을 때
✅ 3️⃣ Not-null Assertion (!!) - 강제 호출 (위험 요소 존재)
val length: Int = name!!.length
- name이 null이면 NullPointerException 발생
val text: String? = null
println(text!!.length) // ❌ NPE 발생 (java.lang.NullPointerException)
💡 언제 사용하면 좋은가?
- 정말 100% 확신할 때만 사용 (예: 외부 API가 절대 null을 반환하지 않는 경우)
- 하지만 가능하면 !! 연산자 대신 ?. 또는 ?: 을 활용하는 것이 더 안전함
근본적으로 !!는 컴파일러에게 "나는 이 값이 null이 아님을 잘 알고 있다. 내가 잘못 생각했다면 예외가 발생해도 감수하겠다."라고 말하는 것이다.
하지만 널 아님 단언문이 더 나은 해법인 경우도 있다. 어떤 함수가 값이 null인지 검사한 다음에 다른 함수를 호출한다고 해도 컴파일러는 호출된 함수 안에서 안전하게 그 값을 사용할 수 있음을 인식할 수 없다. 하지만 이런 경우 호출된 함수가 언제나 다른 함수에서 null이 아닌 값을 전달받는다는 사실이 분명하다면 굳이 null 검사를 다시 수행하고 싶지는 않을 것이다. 이럴 때 널 아님 단언문을 쓸 수 있다.
실무에서는 스윙과 같은 다양한 UI 프레임워크에 있는 액션 클래스에서 이런 일이 자주 발생한다. 액션 클래스 안에는 그 액션의 상태를 변경(활성화 또는 비활성화)하는 메서드와 실제 액션을 실행하는 메서드가 있다. update 메서드 안에서 검사하는 조건을 만족하지 않는 경우 execute 메서드는 호출될 수 없다. 하지만 컴파일러는 그런 연관관계를 알 방법이 없다.
- 코틀린 인 액션, 328p
✅ 4️⃣ let 함수 - 안전한 블록 실행
name?.let {
println("Name is $it")
}
- name이 null이 아닐 때만 블록을 실행
val userInput: String? = readLine()
userInput?.let { println("User input: $it") } // ✅ 입력값이 null이 아닐 때만 실행
💡 언제 사용하면 좋은가?
- null 체크 후 특정 블록을 실행하고 싶을 때
- it 키워드를 사용하여 안전한 값 접근 가능
하지만 let 함수를 통해 인자를 전달할 수도 있다. let 함수는 자신의 수신 객체를 인자로 전달받은 람다에 넘긴다. 널이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되 널이 아닌 타입을 인자로 받는 람다를 let에 전달한다. 이렇게 하면 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 값으로 바꿔 람다에 전달하게 된다.
- 코틀린 인 액션, 331p
🚀 3. 실무에서 Null 안정성을 활용하는 예제
✅ 1️⃣ API 응답을 처리할 때 (?., ?:)
data class User(val name: String?)
fun getUser(): User? = null // API가 null을 반환할 가능성이 있음
fun main() {
val user = getUser()
println(user?.name ?: "Guest") // ✅ 안전한 처리 (null이면 "Guest" 출력)
}
- API 응답이 null일 경우 기본값을 설정 ("Guest")
✅ 2️⃣ JSON 파싱 시 Null 처리 (let)
val json: String? = fetchJson()
json?.let {
println("Parsing JSON: $it") // ✅ JSON이 null이 아닐 때만 실행
}
- JSON 데이터가 있을 때만 파싱 진행 (불필요한 null 체크 방지)
✅ 3️⃣ 안전한 리스트 접근 (?.getOrNull())
val list = listOf("Kotlin", "Java")
val item = list.getOrNull(2) ?: "Default"
println(item) // 출력: Default (인덱스 초과 시 null 대신 기본값 반환)
- 리스트 인덱스 초과 시 예외 발생을 방지하고 기본값 제공
4. 🔍 == vs equals() vs === (객체 비교와 Null 안정성)
연산자 | 설명 | 예제 | 결과 |
== | 값 비교 (equals() 호출) | "hello" == "hello" | ✅ true |
equals() | 값 비교 (명시적 호출) | "hello".equals("hello") | ✅ true |
=== | 참조(메모리 주소) 비교 | user1 === user3 | ⚠️ false (String Constant Pool 이용할 때에는 같은 객체) |
?.equals() | Null-safe 비교 | null?.equals("hello") | ✅ false (NPE 발생 안 함) |
class User(val name: String, val age: Int)
fun main() {
val user1 = User("Alice", 30)
val user2 = User("Alice", 30)
val user3 = user1 // 같은 객체 참조
println(user1 == user2) // ✅ false (equals() 재정의 안 됨 → 참조 비교)
println(user1 === user2) // ❌ false (다른 객체)
println(user1 === user3) // ✅ true (같은 객체 참조)
}
🔥 Null 안정성 정리 요약
기능 | 연산자 | 예제 | 설명 |
Null 허용 타입 | ? | val name: String? | null을 허용하는 타입 지정 |
안전한 호출 | ?. | name?.length | null이면 호출하지 않고 null 반환 |
기본값 제공 | ?: | name ?: "Guest" | null이면 기본값 반환 |
강제 호출 | !! | name!!.length | null이면 NPE 발생 |
블록 실행 | let | name?.let { println(it) } | null이 아닐 때만 실행 |
객체 비교 | ==, === | a == b, a === b | 값 비교 / 참조 비교 |
🤔 추가로 생각해 볼 질문들
- Kotlin에서 !! 연산자를 사용하면 안 되는 경우는 언제일까?
- let과 run의 차이점은 무엇일까?
- 자바의 Optional<T>와 Kotlin의 null-safe 처리 방식의 차이점은?
- list.getOrNull(index)와 list[index]의 차이는?
- == 연산자와 equals() 함수의 내부 구현 차이는?