Theme:

자바 개발자가 가장 무서워하는 예외가 뭐냐고 물으면, 십중팔구 NullPointerException이라고 답할 거다. 코틀린은 이 문제를 언어 차원에서 해결했다. "이 변수가 null일 수 있는가?"를 타입 시스템에 녹여서, 컴파일 타임에 NPE 가능성을 잡아낸다. 면접에서도 코틀린의 Null Safety는 단골 주제이니, 제대로 정리해보자.

Nullable 타입 — 물음표 하나의 차이

코틀린의 모든 타입은 기본적으로 null을 허용하지 않는다.

KOTLIN
var name: String = "코틀린"
// name = null              // 컴파일 에러!

var nullableName: String? = "코틀린"
nullableName = null         // OK

StringString?는 완전히 다른 타입이다. 코틀린 컴파일러는 이 둘을 엄격하게 구분한다.

왜 이렇게 설계했을까?

Tony Hoare가 null reference를 "10억 달러짜리 실수"라고 불렀다는 일화는 유명하다. 코틀린은 null이 필요한 곳에만 명시적으로 ?를 붙이게 해서, 코드를 읽는 것만으로 null 가능성을 파악할 수 있게 만들었다.

KOTLIN
// 함수 시그니처만 봐도 null 가능성을 알 수 있다
fun findUser(id: Long): User?          // null일 수 있음 — 못 찾을 수도 있다
fun getCurrentUser(): User             // null이 아님 — 반드시 존재

안전 호출 연산자(?.) — null이면 건너뛰기

Nullable 타입의 메서드를 호출할 때 ?.를 사용한다. 수신 객체가 null이면 호출을 건너뛰고 null을 반환한다.

KOTLIN
val name: String? = null

// 안전 호출
println(name?.length)       // null 출력 (예외 아님!)
println(name?.uppercase())  // null 출력

체이닝으로 깔끔하게

안전 호출은 체이닝이 가능하다. 중간에 하나라도 null이면 전체 결과가 null이 된다.

KOTLIN
// 자바라면 이렇게 써야 했을 것
// if (user != null && user.getAddress() != null && user.getAddress().getCity() != null) ...

// 코틀린
val city = user?.address?.city   // 어디서든 null이면 결과는 null

공부하다 보니 이 체이닝 패턴이 실무에서 정말 자주 쓰이더라. 자바의 Optional 체이닝과 비슷한 느낌인데, 훨씬 간결하다.

엘비스 연산자(?:) — null일 때 기본값

?.의 결과가 null일 때 기본값을 지정하고 싶으면 엘비스 연산자를 사용한다. 이름이 엘비스인 이유는 ?: 모양이 엘비스 프레슬리의 머리카락을 닮아서라는 설이 있다.

KOTLIN
val name: String? = null

// 기본값 제공
val displayName = name ?: "익명"
println(displayName)          // 익명

// 안전 호출과 조합
val length = name?.length ?: 0
println(length)               // 0

조기 반환 패턴

엘비스 연산자와 return이나 throw를 조합하면 강력한 조기 반환 패턴을 만들 수 있다.

KOTLIN
fun processUser(userId: Long) {
    val user = findUser(userId) ?: return          // 없으면 바로 리턴
    val email = user.email ?: throw IllegalStateException("이메일 필수")

    sendNotification(email)
}

면접에서 이 패턴을 물어보더라고요. "엘비스 연산자를 어디서 활용하냐"는 질문에 이 조기 반환 패턴을 설명하면 좋은 인상을 준다.

!! 연산자 — 강제 Non-null 단언

!!는 "이 값은 절대 null이 아니다"라고 개발자가 단언하는 연산자다. null이면 KotlinNullPointerException이 발생한다.

KOTLIN
val name: String? = "코틀린"
val length = name!!.length    // null이 아니라고 단언

val nullName: String? = null
// val crash = nullName!!.length  // KotlinNullPointerException 발생!

!! 사용을 피해야 하는 이유

!!를 남용하면 코틀린의 Null Safety 장점이 완전히 사라진다. 사실상 자바처럼 NPE가 발생할 수 있게 되는 것이다.

KOTLIN
// 나쁜 예 — !!를 남발
fun getFullName(user: User?): String {
    return user!!.firstName + " " + user!!.lastName  // 위험!
}

// 좋은 예 — 안전 호출과 엘비스 사용
fun getFullName(user: User?): String {
    return "${user?.firstName ?: ""} ${user?.lastName ?: ""}".trim()
}

실무에서 !!가 정당화되는 경우는 거의 없다. 코드 리뷰에서 !!가 보이면 "이걸 왜 nullable로 선언했는지"부터 재검토하는 게 맞다.

let/also로 null 처리 체이닝

스코프 함수와 안전 호출을 조합하면 null 분기를 매우 깔끔하게 처리할 수 있다.

let — null이 아닐 때만 실행

KOTLIN
val email: String? = getEmail()

// null이 아닐 때만 블록 실행
email?.let { validEmail ->
    sendVerification(validEmail)
    println("인증 메일 발송: $validEmail")
}

// 엘비스와 조합
val result = email?.let { processEmail(it) } ?: "이메일 없음"

also — 부수 효과(로깅 등)

KOTLIN
val user = findUser(userId)?.also { u ->
    logger.info("사용자 조회 성공: ${u.name}")
}

체이닝 패턴

KOTLIN
fun processOrder(orderId: Long): String {
    return findOrder(orderId)
        ?.let { order -> validateOrder(order) }
        ?.also { validated -> logger.info("주문 검증 완료: ${validated.id}") }
        ?.let { validated -> completeOrder(validated) }
        ?: "주문을 찾을 수 없습니다"
}

이 패턴이 처음엔 낯설 수 있는데, 익숙해지면 null 분기를 if-else 없이 깔끔하게 작성할 수 있다.

스마트 캐스트 — null 검사 후 자동 변환

코틀린 컴파일러는 null 검사 이후에 자동으로 타입을 Non-null로 변환해준다. 이걸 스마트 캐스트라고 한다.

KOTLIN
fun printLength(text: String?) {
    if (text != null) {
        // 여기서 text는 자동으로 String 타입 (Non-null)
        println(text.length)      // ?. 없이 직접 호출 가능
    }
}

// when과 조합
fun describe(value: Any?) {
    when (value) {
        null -> println("null입니다")
        is String -> println("문자열 길이: ${value.length}")  // 스마트 캐스트
        is Int -> println("정수 값: $value")
    }
}

자바 코드와의 상호운용 — Platform Type

코틀린의 Null Safety가 빛나는 건 순수 코틀린 코드일 때다. 자바 코드가 섞이면 이야기가 달라진다.

Platform Type이란?

자바에서 넘어온 타입 중 @Nullable이나 @NotNull 어노테이션이 없는 타입은 Platform Type이 된다. IDE에서는 String!로 표시된다.

JAVA
// 자바 코드 — 어노테이션 없음
public class JavaUtils {
    public static String getName() {
        return null;  // null을 반환할 수 있음
    }
}
KOTLIN
// 코틀린에서 호출
val name = JavaUtils.getName()   // String! (Platform Type)
// name.length                    // 런타임에 NPE 가능!

안전하게 처리하는 방법

KOTLIN
// 방법 1: Nullable로 받기 (권장)
val name: String? = JavaUtils.getName()
println(name?.length ?: 0)

// 방법 2: Non-null로 받기 (확신이 있을 때만)
val name: String = JavaUtils.getName()   // null이면 즉시 예외

@Nullable/@NotNull 어노테이션

자바 코드에 어노테이션을 붙이면 코틀린 컴파일러가 인식한다.

JAVA
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;

public class JavaUtils {
    @Nullable
    public static String getNullableName() { return null; }

    @NotNull
    public static String getNotNullName() { return "Java"; }
}
KOTLIN
val nullable = JavaUtils.getNullableName()   // String? 로 추론
val notNull = JavaUtils.getNotNullName()     // String 으로 추론

면접에서 "코틀린의 Null Safety 한계가 뭐냐"라는 질문이 나오면, 이 Platform Type 이야기를 하면 된다. 자바와 혼용할 때는 완벽한 Null Safety가 보장되지 않는다는 점을 솔직하게 인정하되, 어노테이션으로 보완할 수 있다고 덧붙이면 좋다.

자바 Optional vs 코틀린 Nullable

자바 개발자라면 Optional과 비교가 궁금할 거다.

특성Java OptionalKotlin Nullable
적용 범위반환 타입에만 권장모든 곳에 적용
성능래퍼 객체 생성추가 객체 없음
문법.orElse(), .map()?., ?:, let
필드 사용권장하지 않음자유롭게 사용
컬렉션 원소권장하지 않음자유롭게 사용

코틀린의 Nullable은 타입 시스템에 내장되어 있어서 추가 객체 생성 비용이 없다. Optional처럼 래퍼로 감싸는 방식이 아니라, 컴파일러 레벨에서 null 검사를 강제하는 방식이다.

정리

코틀린의 Null Safety는 단순히 편의 기능이 아니라 타입 시스템의 핵심이다.

  • Nullable 타입(?): null 가능성을 타입으로 명시
  • 안전 호출(?.): null이면 건너뛰고 null 반환
  • 엘비스 연산자(?:): null일 때 기본값 제공, 조기 반환 패턴
  • !! 연산자: 최후의 수단, 남용 금지
  • let/also 체이닝: 스코프 함수와 조합해 깔끔한 null 처리
  • Platform Type: 자바 코드와의 상호운용 시 주의 필요

면접에서 핵심은 "코틀린이 NPE를 어떻게 방지하느냐"보다 "어떤 한계가 있고 어떻게 보완하느냐"까지 답할 수 있어야 한다는 것이다. Platform Type과 어노테이션까지 언급하면 깊이가 다르다.

댓글 로딩 중...