1. 개요
자바를 쓰다 코틀린으로 넘어오면 가장 먼저 부딪히는 게 물음표다. String 과 String? 이 왜 다른 타입인지, ?. 와 ?: 는 뭐가 다른지, !! 는 왜 쓰지 말라고들 하는지. NPE(NullPointerException)를 만든 토니 호어 본인이 “10억 달러짜리 실수”라고 부른 그 문제를, 코틀린은 타입 시스템 안으로 끌고 들어와 풀었다. 이 글은 그 설계가 실제로 코드에서 어떻게 강제되는지, 그리고 그 강제를 몰래 풀어버리는 지점들을 짚는다.
2. 핵심 내용
2-1. String 과 String? 은 다른 타입이다
자바에서 String name 은 null 이 들어갈 수 있는지 타입만 봐서는 알 수 없다. @Nullable 어노테이션을 안 붙이면 컴파일러는 아무 말도 안 하고, 런타임에 name.length() 를 부르다 NPE 를 맞는다.
코틀린은 이걸 타입 레벨로 갈랐다.
val name: String = "junho" // null 불가능
val nickname: String? = null // null 가능name.length 는 컴파일된다. nickname.length 는 컴파일 에러다. 널이 될 수 있는 값을 null 처리 없이 쓰는 코드는 애초에 컴파일이 안 된다. NPE 를 런타임 문제에서 컴파일 타임 문제로 옮긴 것이 코틀린 null 안전성의 핵심이다.
2-2. ?. — 세이프 콜
nickname?.length 는 nickname 이 null 이 아니면 length 를 평가하고, null 이면 전체 식이 null 이 된다. 체이닝도 된다.
val length = user?.profile?.nickname?.length중간 어디서든 null 이면 그 뒤는 평가 안 하고 전체가 null 이다. 자바의 중첩 if (a != null && a.b != null && ...) 를 한 줄로 줄인다.
2-3. ?: — 엘비스 연산자
?. 가 만든 null 을 기본값으로 받는 게 ?: 다.
val length = nickname?.length ?: 0“nickname 이 null 이면 0” 이라는 뜻이다. 왼쪽이 null 이 아니면 왼쪽 값, null 이면 오른쪽 값을 쓴다. 예외를 던지는 것도 가능하다.
val user = repository.findById(id) ?: throw NotFoundException(id)이 패턴을 쓰면 이후 코드에서 user 는 non-null 타입으로 스마트캐스트된다. null 체크와 조기 반환을 한 줄로 합치면서 타입도 좁혀지는 것이 ?: 를 자주 쓰는 이유다.
2-4. !! — 컴파일러에게 하는 거짓말
!! 는 “이거 null 아닌 거 내가 보장한다” 는 뜻이다. null 이면 그 자리에서 NPE 를 던진다. 즉 !! 는 코틀린이 막아준 문제를 자바 시절로 되돌리는 연산자다.
val length = nickname!!.length // nickname 이 null 이면 여기서 NPE이게 위험한 이유는 컴파일러의 보증이 사람의 확신으로 바뀌기 때문이다. “지금은 null 아닐 거야” 는 다음 리팩토링에서 깨진다. !! 가 코드베이스에 많으면 코틀린을 쓰는 의미가 없다. null 이 아님을 보장하고 싶으면 ?: 로 예외를 던지거나, 애초에 그 값이 null 이 될 수 없게 설계를 바꾸는 게 맞다. !! 를 써도 되는 몇 안 되는 경우는 프레임워크 제약으로 non-null 을 선언해야 하는데 초기화 시점이 늦는 경우 정도이고, 그때도 lateinit 이 먼저다.
2-5. 플랫폼 타입 — 자바와 만나는 지점
코틀린은 자바 코드와 섞여 돈다. 자바 메서드가 돌려주는 String 은 코틀린 쪽에서 String! 로 보인다. null 가능 여부를 모른다는 뜻의 “플랫폼 타입” 이다. 코틀린 컴파일러는 여기선 null 체크를 강제하지 않는다. 자바 라이브러리 호출 결과를 아무 검증 없이 non-null 로 받아쓰면, 널 안전성은 그 경계에서 조용히 뚫린다. 자바 상호운용 코드에서 NPE 가 나는 자리는 대부분 여기다.
3. 마무리
요약
String과String?은 서로 다른 타입이고, null 처리 없이 nullable 값을 쓰면 컴파일이 안 된다.?.는 null 이면 평가를 건너뛰고,?:는 null 일 때의 기본값(또는 예외)을 정한다.?: throw뒤에는 값이 non-null 로 스마트캐스트된다.!!는 컴파일러의 보증을 사람의 확신으로 바꾼다. 남발하면 코틀린을 쓰는 의미가 없다.- 자바 경계의 플랫폼 타입(
String!)은 null 안전성이 미치지 않는 지점이다.