Null 안전성
Null Safety
null 참조로 인한 오류를 컴파일 타임에 방지하는 타입 시스템 기능. Kotlin, Swift, TypeScript 등 현대 언어에서 NullPointerException을 원천 차단합니다.
Null Safety
null 참조로 인한 오류를 컴파일 타임에 방지하는 타입 시스템 기능. Kotlin, Swift, TypeScript 등 현대 언어에서 NullPointerException을 원천 차단합니다.
Null Safety(널 안전성)는 타입 시스템 차원에서 null 참조로 인한 런타임 오류를 컴파일 타임에 방지하는 프로그래밍 언어 기능입니다. Tony Hoare가 "Billion Dollar Mistake"라고 부른 NullPointerException(NPE)을 원천적으로 차단하기 위해 설계되었습니다. 변수가 null 값을 가질 수 있는지 여부를 타입으로 명시하고, 컴파일러가 이를 강제함으로써 런타임 크래시를 예방합니다.
언어별 구현을 살펴보면, Kotlin은 모든 타입이 기본적으로 non-null이며 String?처럼 ?를 붙여 nullable을 명시합니다. Swift는 Optional 타입(String?)과 강제 언래핑(!), 옵셔널 바인딩(if let)을 제공합니다. TypeScript는 strictNullChecks 옵션으로 null/undefined를 별도 타입으로 취급하며, Dart는 2.12 버전부터 sound null safety를 도입했습니다.
Optional/Nullable 타입은 "값이 있거나 없을 수 있음"을 타입으로 표현합니다. Java의 Optional<T>, Rust의 Option<T>, Haskell의 Maybe a가 대표적입니다. 이러한 래퍼 타입은 null을 직접 다루지 않고 map, flatMap, orElse 같은 메서드를 통해 안전하게 값을 처리합니다. 함수형 프로그래밍의 모나드 개념에서 유래했습니다.
컴파일 타임 vs 런타임 체크의 핵심 차이는 버그 발견 시점입니다. 런타임 체크는 실제 실행 중에 null을 만나야 오류가 발생하지만, 컴파일 타임 체크는 코드 작성 단계에서 null 가능성을 검증합니다. Kotlin, Swift, Rust는 컴파일 타임에 null safety를 강제하여 NPE가 원칙적으로 발생하지 않습니다. 반면 Java의 Optional은 런타임 래퍼이므로 Optional.get() 남용 시 여전히 예외가 발생할 수 있습니다.
// 기본적으로 모든 타입은 non-null var name: String = "Alice" // name = null // 컴파일 에러! // Nullable 타입은 ?를 붙여 명시 var nickname: String? = null // OK // Safe call operator (?.) val length = nickname?.length // null이면 null 반환, Int? // Elvis operator (?:) - 기본값 제공 val len = nickname?.length ?: 0 // null이면 0 // Smart cast - null 체크 후 자동 타입 변환 if (nickname != null) { println(nickname.length) // 자동으로 String으로 취급 } // let을 이용한 안전한 처리 nickname?.let { safeName -> println("Nickname: $safeName") } // 강제 언래핑 (!!) - 가급적 피해야 함 val forceLength = nickname!!.length // NPE 발생 가능! // 안전한 타입 캐스팅 val str: String? = value as? String // 실패 시 null
// tsconfig.json: "strictNullChecks": true // null/undefined를 명시적으로 허용해야 함 let name: string = "Alice"; // name = null; // 컴파일 에러! let nickname: string | null = null; // OK let age: number | undefined; // undefined 허용 // Optional Chaining (?.) interface User { name: string; address?: { city: string; zip?: string; }; } function getCity(user: User): string | undefined { return user.address?.city; // 안전하게 접근 } // Nullish Coalescing (??) const displayName = nickname ?? "Anonymous"; // Type Guard로 타입 좁히기 function processName(name: string | null): string { if (name === null) { return "Guest"; } return name.toUpperCase(); // 여기서 name은 string } // Non-null assertion (!) - 확실할 때만 사용 const element = document.getElementById("app")!; // element가 null이 아님을 개발자가 보장
// Optional 선언 var name: String? = nil // Optional Binding (if let) if let unwrappedName = name { print("Name: \(unwrappedName)") } else { print("No name") } // Guard let - 조기 반환 패턴 func greet(_ name: String?) { guard let name = name else { print("No name provided") return } print("Hello, \(name)!") } // Nil-Coalescing Operator (??) let displayName = name ?? "Anonymous" // Optional Chaining let count = name?.count // Int? // 강제 언래핑 (!) - 위험! // let forced = name! // nil이면 크래시
Optional이 있지 않나요? 그걸로 충분하지 않을까요?
.get()을 직접 호출하면 여전히 예외가 발생하고, 필드나 파라미터에 쓰면 오히려 더 복잡해져요. Kotlin은 타입 시스템 자체에 null safety가 내장되어 있어서 ? 하나로 간결하게 처리되고, 컴파일러가 빠뜨린 null 체크를 잡아줍니다.
?.let { } 패턴이랑 Elvis 연산자가 코드를 훨씬 깔끔하게 만들어줄 것 같네요.
user?.address?.city ?: "Unknown"처럼 한 줄로 안전하게 체이닝할 수 있고, Java처럼 중첩 if문으로 null 체크를 할 필요가 없습니다.
!! 연산자는 언제 쓰나요?
!!는 "나는 이 값이 null이 아님을 100% 확신한다"는 의미인데, 실제로는 거의 사용하지 않습니다. 대부분의 경우 ?.let, ?:, 또는 requireNotNull()로 더 안전하게 처리할 수 있어요. 코드 리뷰에서 !!가 보이면 "정말 필요한가?"를 재검토합니다.
!!나 Swift의 !는 NPE/크래시의 직접적인 원인입니다. "절대 null이 아니다"라는 확신은 대부분 틀리므로, ?.let, ?:, guard let 등 안전한 대안을 사용하세요.?:)나 orElse()로 null일 때의 기본값을 명시하세요. 빈 문자열("")과 null, 빈 리스트와 null은 의미가 다르므로 비즈니스 로직에 맞는 기본값을 선택해야 합니다.String!)에 주의하고, @Nullable/@NonNull 어노테이션을 확인하세요.if (x != null) 이후 블록에서는 non-null 타입으로 자동 변환됩니다.Optional.get() 직접 호출은 null safety를 무력화합니다.