🛡️ 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을 알아보세요!