본문으로 건너뛰기

🛡️ Null 안전성

📖 Null 안전성이란?

**Null 안전성(Null Safety)**은 NullPointerException을 컴파일 시점에 방지하는 Kotlin의 핵심 기능입니다.

💡 Nullable 타입

Non-null vs Nullable

fun main() {
// Non-null (기본)
var name: String = "홍길동"
// name = null // ❌ 오류!

// Nullable (? 추가)
var nullableName: String? = "홍길동"
nullableName = null // ✅ 가능
}

Nullable 타입 비교

fun main() {
val name: String = "홍길동" // Non-null
val age: Int = 25 // Non-null

val email: String? = null // Nullable
val phone: Int? = null // Nullable

println(name.length) // ✅
// println(email.length) // ❌ 오류!
}

🔧 안전한 호출

?. 연산자

fun main() {
val name: String? = null

// 안전한 호출
val length = name?.length
println(length) // null

val upper = name?.uppercase()
println(upper) // null
}

체이닝

data class Address(val city: String?)
data class Person(val address: Address?)

fun main() {
val person: Person? = Person(Address("서울"))

// 안전한 체이닝
val city = person?.address?.city
println(city) // 서울

val nobody: Person? = null
val noCity = nobody?.address?.city
println(noCity) // null
}

🎯 엘비스 연산자

?: 기본값

fun main() {
val name: String? = null

// null이면 기본값 사용
val displayName = name ?: "이름 없음"
println(displayName) // 이름 없음

val validName: String? = "홍길동"
val display = validName ?: "이름 없음"
println(display) // 홍길동
}

early return

fun processUser(name: String?) {
val validName = name ?: run {
println("이름이 없습니다")
return
}

println("처리: $validName")
}

fun main() {
processUser("홍길동") // 처리: 홍길동
processUser(null) // 이름이 없습니다
}

🔍 !! 연산자

강제 언래핑

fun main() {
val name: String? = "홍길동"

// null이 아님을 확신할 때 (주의!)
val length = name!!.length
println(length) // 3

val nullName: String? = null
// val error = nullName!!.length // ❌ NPE 발생!
}

언제 사용?

// ❌ 나쁜 사용
fun bad(name: String?) {
println(name!!.length) // 위험!
}

// ✅ 좋은 사용 (확실한 경우만)
fun good(name: String?) {
if (name != null) {
println(name.length) // 안전
}
}

🎨 let 함수

null이 아닐 때만 실행

fun main() {
val name: String? = "홍길동"

name?.let {
println("이름: $it")
println("길이: ${it.length}")
}
// 이름: 홍길동
// 길이: 3

val nullName: String? = null
nullName?.let {
println("실행 안 됨")
}
}

체이닝

fun main() {
val name: String? = "홍길동"

val result = name
?.uppercase()
?.let { "안녕하세요, ${it}님!" }
?: "이름 없음"

println(result) // 안녕하세요, 홍길동님!
}

🎯 실전 예제

사용자 정보 출력

data class User(
val name: String?,
val email: String?,
val age: Int?
)

fun printUserInfo(user: User?) {
val name = user?.name ?: "이름 없음"
val email = user?.email ?: "이메일 없음"
val age = user?.age ?: 0

println("""
이름: $name
이메일: $email
나이: ${age}
""".trimIndent())
}

fun main() {
printUserInfo(User("홍길동", "hong@example.com", 25))
println()
printUserInfo(User(null, null, null))
println()
printUserInfo(null)
}

설정 관리

data class Config(
val host: String?,
val port: Int?,
val timeout: Int?
) {
fun getUrl(): String {
val validHost = host ?: "localhost"
val validPort = port ?: 8080
return "http://$validHost:$validPort"
}

fun getTimeout(): Int = timeout ?: 3000
}

fun main() {
val config1 = Config("example.com", 9090, 5000)
println(config1.getUrl()) // http://example.com:9090
println(config1.getTimeout()) // 5000

val config2 = Config(null, null, null)
println(config2.getUrl()) // http://localhost:8080
println(config2.getTimeout()) // 3000
}

안전한 변환

fun parseAge(input: String?): Int {
return input?.toIntOrNull() ?: 0
}

fun parseEmail(input: String?): String? {
return input?.takeIf { it.contains("@") }
}

fun main() {
println(parseAge("25")) // 25
println(parseAge("abc")) // 0
println(parseAge(null)) // 0

println(parseEmail("hong@example.com")) // hong@example.com
println(parseEmail("invalid")) // null
println(parseEmail(null)) // null
}

리스트 필터링

fun main() {
val names: List<String?> = listOf("Alice", null, "Bob", null, "Charlie")

// null 제거
val validNames = names.filterNotNull()
println(validNames) // [Alice, Bob, Charlie]

// 안전하게 처리
names.forEach { name ->
name?.let {
println("안녕하세요, $it님!")
}
}
}

🔍 타입 체크와 캐스트

스마트 캐스트

fun main() {
val value: Any? = "Hello"

if (value is String) {
// 자동으로 String으로 캐스트
println(value.length) // ✅
}

if (value != null) {
// 자동으로 Non-null로 캐스트
println(value.toString())
}
}

안전한 캐스트

fun main() {
val value: Any? = "Hello"

// as? - 실패하면 null
val str = value as? String
println(str) // Hello

val num = value as? Int
println(num) // null
}

🤔 자주 묻는 질문

Q1. Nullable을 언제 쓰나요?

A: 값이 없을 수 있을 때!

// ✅ Nullable이 적절한 경우
data class User(
val name: String, // 필수
val email: String?, // 선택
val phone: String? // 선택
)

// ❌ 무분별한 Nullable
data class Product(
val name: String?, // 이름은 필수인데?
val price: Int? // 가격도 필수인데?
)

Q2. ?.과 !!의 차이는?

A: 안전 vs 위험!

fun main() {
val name: String? = null

// ?. - 안전 (권장)
val safe = name?.length // null 반환
println(safe)

// !! - 위험 (피하기)
// val unsafe = name!!.length // NPE 발생!
}

Q3. lateinit은 뭔가요?

A: 나중에 초기화!

class Example {
// null 불가, 나중에 초기화
lateinit var name: String

fun init() {
name = "홍길동"
}

fun print() {
if (::name.isInitialized) {
println(name)
} else {
println("초기화 안 됨")
}
}
}

fun main() {
val example = Example()
example.print() // 초기화 안 됨
example.init()
example.print() // 홍길동
}

Q4. by lazy는?

A: 처음 사용할 때 초기화!

class Example {
// 처음 접근할 때 초기화
val expensiveValue: String by lazy {
println("초기화 중...")
"값"
}
}

fun main() {
val example = Example()
println("생성됨")
println(example.expensiveValue) // 여기서 초기화
println(example.expensiveValue) // 재사용
}

🎬 마치며

Null 안전성으로 안정적인 코드를 작성하세요!

핵심 정리:
✅ ? 로 Nullable 타입
✅ ?. 로 안전한 호출
✅ ?: 로 기본값 제공
✅ !! 는 피하기
✅ let으로 null 체크

다음 단계: 컬렉션에서 List, Set, Map을 알아보세요!