2025년 12월 31일 작성
Safe Call Operator (
Elvis Operator (
Non-null Assertion (
Safe Cast (
Kotlin Null Safety - Nullable Type과 안전한 호출
Kotlin은 type system에서 nullable과 non-null을 구분하여 compile time에 NullPointerException을 방지합니다.
Null Safety
- Kotlin은 type system 수준에서 null 가능 여부를 구분합니다.
- nullable type과 non-null type을 명확히 분리합니다.
- compile time에 null 관련 오류를 감지하여
NullPointerException을 방지합니다.
flowchart LR
subgraph type_system[Type System]
nonnull[Non-null Type]
nullable[Nullable Type]
end
nonnull -->|"String"| safe[Null 안전]
nullable -->|"String?"| check[Null check 필요]
- Java에서 흔히 발생하는
NullPointerException을 언어 차원에서 예방합니다.
Nullable Type과 Non-null Type
- 기본적으로 모든 type은 null을 허용하지 않습니다.
- null을 허용하려면 type 뒤에
?를 붙입니다.
- null을 허용하려면 type 뒤에
// non-null type
var name: String = "Kotlin"
// name = null // compile error
// nullable type
var nickname: String? = "K"
nickname = null // 가능
- nullable type은 null check 없이 직접 사용할 수 없습니다.
val nickname: String? = "Kotlin"
// compile error : nullable type에 직접 접근 불가
// val length = nickname.length
// null check 후 사용
if (nickname != null) {
val length = nickname.length // smart cast로 String으로 처리
}
Safe Call Operator (?.)
- null이면 null을 반환하고, 아니면 연산을 수행합니다.
- null check와 method 호출을 한 번에 처리합니다.
val name: String? = "Kotlin"
val nullName: String? = null
println(name?.length) // 6
println(nullName?.length) // null
- chaining으로 여러 단계의 null check를 간결하게 처리합니다.
val city: String? = user?.address?.city
// 위 code는 아래와 동일
val city: String? = if (user != null && user.address != null) {
user.address.city
} else {
null
}
Elvis Operator (?:)
- null일 때 대체값을 지정합니다.
- 좌측이 null이 아니면 좌측 값, null이면 우측 값을 반환합니다.
val name: String? = null
val displayName = name ?: "Unknown" // "Unknown"
val length = name?.length ?: 0 // 0
- early return이나 exception throw와 함께 사용할 수 있습니다.
fun process(name: String?) {
val validName = name ?: return // null이면 함수 종료
println(validName)
}
fun getLength(name: String?): Int {
val validName = name ?: throw IllegalArgumentException("Name is required")
return validName.length
}
Non-null Assertion (!!)
- null이 아님을 단언합니다.
- null이면
NullPointerException이 발생합니다. - 가능하면 사용을 피하는 것이 좋습니다.
- null이면
val name: String? = "Kotlin"
val length = name!!.length // 6
val nullName: String? = null
// val error = nullName!!.length // NullPointerException 발생
!!사용이 적절한 경우는 제한적입니다.- 외부에서 null이 아님이 보장되지만 compiler가 인식하지 못하는 경우.
- test code에서 의도적으로 NPE를 발생시키는 경우.
Safe Cast (as?)
- casting 실패 시 null을 반환합니다.
- 일반 cast(
as)는 실패 시ClassCastException을 발생시킵니다.
- 일반 cast(
val obj: Any = "Kotlin"
val str: String? = obj as? String // "Kotlin"
val num: Int? = obj as? Int // null (casting 실패)
- elvis operator와 함께 사용하면 유용합니다.
val length = (obj as? String)?.length ?: 0
let 함수와 Null 처리
let과 safe call을 조합하여 null이 아닐 때만 block을 실행합니다.
val name: String? = "Kotlin"
name?.let {
println("Name is $it") // "Name is Kotlin"
println("Length is ${it.length}") // "Length is 6"
}
val nullName: String? = null
nullName?.let {
println("This won't print") // 실행되지 않음
}
- nullable 값을 non-null로 변환할 때 유용합니다.
val input: String? = readLine()
val result = input?.let { value ->
// value는 non-null String
value.trim().uppercase()
} ?: "DEFAULT"
lateinit과 Null Safety
lateinit은 non-null 변수의 초기화를 지연합니다.- nullable type 대신 사용하여 null check를 피할 수 있습니다.
- 초기화 전 접근 시
UninitializedPropertyAccessException이 발생합니다.
class UserService {
lateinit var repository: UserRepository
fun init(repo: UserRepository) {
repository = repo
}
fun getUser(id: Long): User {
// null check 없이 사용 가능
return repository.findById(id)
}
}
isInitialized로 초기화 여부를 확인할 수 있습니다.
if (::repository.isInitialized) {
repository.findById(id)
}
Platform Type
- Java code에서 온 type은 null 가능 여부를 알 수 없습니다.
- Kotlin compiler는 이를 platform type으로 처리합니다.
String!처럼 표기되며, nullable로도 non-null로도 사용할 수 있습니다.
// Java method : String getName() { return null; }
val name = javaObject.getName() // String! (platform type)
// 두 가지 모두 가능하지만 위험
val length1 = name.length // NPE 발생 가능
val length2 = name?.length // 안전
- Java code와 상호 운용 시 주의해야 합니다.
- Java의
@Nullable,@NotNullannotation이 있으면 Kotlin이 인식합니다.
- Java의
// Java
@NotNull
public String getName() { return "Kotlin"; }
@Nullable
public String getNickname() { return null; }
// Kotlin
val name: String = obj.getName() // non-null로 인식
val nickname: String? = obj.getNickname() // nullable로 인식
Null Safety Best Practice
- nullable type 사용을 최소화합니다.
- 가능하면 non-null type을 기본으로 설계합니다.
- nullable이 필요한 경우에만
?를 사용합니다.
!!사용을 피합니다.?.,?:,let등 안전한 대안을 우선 사용합니다.- 불가피한 경우에만 명확한 이유와 함께 사용합니다.
- early return pattern을 활용합니다.
fun process(user: User?) {
val validUser = user ?: return
// 이후 validUser는 non-null
println(validUser.name)
}
- collection의 null 처리에 주의합니다.
// List<String>? : list 자체가 null 가능
// List<String?> : list 요소가 null 가능
// List<String?>? : 둘 다 null 가능
val list: List<String?> = listOf("A", null, "B")
val filtered = list.filterNotNull() // List<String>
Reference
- https://kotlinlang.org/docs/null-safety.html
- https://kotlinlang.org/docs/java-interop.html#null-safety-and-platform-types