💻 프로그래밍

Null 안전성

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)을 제공합니다. TypeScriptstrictNullChecks 옵션으로 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() 남용 시 여전히 예외가 발생할 수 있습니다.

💻 코드 예제

Kotlin - Null Safety 핵심 문법
// 기본적으로 모든 타입은 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
TypeScript - strictNullChecks 모드
// 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이 아님을 개발자가 보장
Swift - Optional 패턴
// 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이면 크래시

🗣 실무 대화 예시

💼 회의: 신규 프로젝트 타입 시스템 선택
👨‍💻
테크 리드
새 백엔드 프로젝트 언어를 정해야 하는데, Java와 Kotlin 중 어떤 게 나을까요? 기존 Java 코드베이스가 있긴 한데...
👩‍💻
백엔드 개발자
저는 Kotlin을 추천드려요. 가장 큰 이유가 Null Safety입니다. 현재 프로덕션에서 발생하는 버그 중 상당수가 NPE거든요. Kotlin은 컴파일 단계에서 null 체크를 강제해서 이런 버그를 원천 차단할 수 있어요.
👨‍💻
테크 리드
Java도 Optional이 있지 않나요? 그걸로 충분하지 않을까요?
👩‍💻
백엔드 개발자
Java의 Optional은 런타임 래퍼여서 강제성이 없어요. 개발자가 .get()을 직접 호출하면 여전히 예외가 발생하고, 필드나 파라미터에 쓰면 오히려 더 복잡해져요. Kotlin은 타입 시스템 자체에 null safety가 내장되어 있어서 ? 하나로 간결하게 처리되고, 컴파일러가 빠뜨린 null 체크를 잡아줍니다.
👨‍💻
테크 리드
좋아요. 기존 Java 코드와 호환도 되고, 점진적 마이그레이션도 가능하니 Kotlin으로 가죠. 특히 ?.let { } 패턴이랑 Elvis 연산자가 코드를 훨씬 깔끔하게 만들어줄 것 같네요.

🎓 기술 면접: Null Safety 장점
👤
면접관
Kotlin의 Null Safety가 왜 중요한지 설명해 주시겠어요? 실제로 어떤 장점이 있나요?
👩
지원자
Null Safety의 가장 큰 장점은 버그 예방입니다. NullPointerException은 가장 흔한 런타임 오류인데, Kotlin에서는 컴파일 타임에 잡아내서 프로덕션까지 가기 전에 수정할 수 있어요.
👩
지원자
두 번째로 코드 가독성이 좋아져요. user?.address?.city ?: "Unknown"처럼 한 줄로 안전하게 체이닝할 수 있고, Java처럼 중첩 if문으로 null 체크를 할 필요가 없습니다.
👤
면접관
!! 연산자는 언제 쓰나요?
👩
지원자
!!는 "나는 이 값이 null이 아님을 100% 확신한다"는 의미인데, 실제로는 거의 사용하지 않습니다. 대부분의 경우 ?.let, ?:, 또는 requireNotNull()로 더 안전하게 처리할 수 있어요. 코드 리뷰에서 !!가 보이면 "정말 필요한가?"를 재검토합니다.

주의사항

🚨 Null Safety 관련 실무 주의사항
  • 강제 언래핑(!!, !) 금지: Kotlin의 !!나 Swift의 !는 NPE/크래시의 직접적인 원인입니다. "절대 null이 아니다"라는 확신은 대부분 틀리므로, ?.let, ?:, guard let 등 안전한 대안을 사용하세요.
  • 적절한 기본값 설정: Elvis 연산자(?:)나 orElse()로 null일 때의 기본값을 명시하세요. 빈 문자열("")과 null, 빈 리스트와 null은 의미가 다르므로 비즈니스 로직에 맞는 기본값을 선택해야 합니다.
  • 외부 API null 처리: REST API 응답, 데이터베이스 쿼리 결과, 서드파티 라이브러리는 항상 null을 반환할 수 있습니다. Java 라이브러리를 Kotlin에서 사용할 때 플랫폼 타입(String!)에 주의하고, @Nullable/@NonNull 어노테이션을 확인하세요.
  • 타입 좁히기 활용: TypeScript의 type guard, Kotlin의 smart cast를 활용하면 명시적 캐스팅 없이 안전하게 타입을 좁힐 수 있습니다. if (x != null) 이후 블록에서는 non-null 타입으로 자동 변환됩니다.
  • Optional 남용 금지: Java의 Optional은 반환 타입용으로 설계되었습니다. 필드, 파라미터, 컬렉션 요소로 사용하면 오히려 복잡해지고, Optional.get() 직접 호출은 null safety를 무력화합니다.

🔗 관련 용어

📚 더 배우기