코틀린은 null 가능 여부를 타입에 넣었다. 기본은 not-null이다.
var a: String = "hi"
a = null // 컴파일 에러
var b: String? = "hi" // ? 를 붙여야 null 을 담을 수 있다
b = null // OK
b.length // 컴파일 에러 — null 일 수 있으니 그냥 못 쓴다
"실행하기 전에 막는다" 가 핵심이다. 자바에서 런타임에 터지던 NPE가 컴파일 오류가 된다.
세 가지 연산자
// 안전 호출 — null 이면 전체가 null
val len: Int? = b?.length
val city = user?.address?.city // 중간에 하나라도 null 이면 통째로 null
// Elvis — null 일 때의 기본값
val len: Int = b?.length ?: 0
val name = user?.name ?: throw IllegalStateException("사용자 없음")
val id = user?.id ?: return // 조기 반환에도 쓴다
// not-null 단언 — null 이면 NPE 를 던진다
val len: Int = b!!.length // 코드 스멜. 최후의 수단
!!를 쓰게 되는 순간이 설계 신호다
"여기는 절대 null 이 아니다" 를 컴파일러에게 강요하는 것이다
- 정말 아니라면 타입을 not-null 로 바꿀 수 있어야 한다
- 못 바꾼다면 그 확신에 근거가 없다는 뜻이다
대안: 안전 호출 · Elvis · require/checkNotNull(의도를 메시지로 남긴다)
val user = checkNotNull(findUser(id)) { "사용자 $id 를 찾을 수 없다" }
스마트 캐스트
if (b != null) {
println(b.length) // 컴파일러가 기억한다. b!! 가 필요 없다
}
if (x is String) {
println(x.uppercase()) // 타입 검사 후 자동 캐스트
}
단 var 프로퍼티에는 안 된다 — 검사 이후 다른 스레드가 바꿀 수 있기 때문이다. 지역 변수에 담아 쓰면 된다.
val v = someVarProperty ?: return // 지역 val 로 고정한 뒤 쓴다
let — null 아닐 때만 실행
user?.let {
println(it.name) // user 가 null 이 아닐 때만 블록이 돈다
}
면접 함정
- ❌ "코틀린은 NPE가 없다" →
!!·플랫폼 타입·초기화 전lateinit접근에서 난다. - ❌ "?. 를 쓰면 안전하다" → 결과가 null이 되므로 그다음을 처리해야 한다. 사슬 끝에서 무시하면 버그가 조용히 숨는다.
컬렉션과 null
val a: List<String>? = null // 리스트 자체가 null 일 수 있다
val b: List<String?> = listOf(null) // 원소가 null 일 수 있다
val c: List<String?>? = null // 둘 다
// 유용한 확장
list.filterNotNull() // null 원소 제거
list.mapNotNull { it.parseOrNull() } // 변환하며 null 제거
nullableList.orEmpty() // null 이면 빈 리스트
nullableList?.isNotEmpty() == true // null 이면 false (?: false 와 같다)
자주 쓰는 판별 함수
text.isNullOrBlank() // null 이거나 공백만
list.isNullOrEmpty()
value ?: return // 조기 반환
value ?: error("없음") // 예외로 종료
컴파일러가 못 잡는 지점
- ① 플랫폼 타입 (자바 경계)
- ② !! 를 쓴 곳
- ③ lateinit 초기화 전 접근
- ④ 초기화 중 열린 메서드 호출
- 생성자에서 open 메서드를 부르면 하위 클래스의 필드가 아직 null 이다
- ⑤ 리플렉션 · 역직렬화
- Gson 등이 not-null 필드에 null 을 넣을 수 있다 (생성자를 우회한다)
⑤가 특히 조용하다 — 타입은 String인데 실제로 null이 들어와 있어, 한참 뒤 엉뚱한 곳에서 터진다. kotlinx.serialization은 이 문제가 없다.