Null Safety란 무엇인가? 개념부터 실전 예시까지
1. Null Safety의 정의
**Null Safety(널 안전성)**란 프로그램이 실행되는 도중에 null(혹은 nil, None 등 언어별 명칭은 다르지만 “값이 없음"을 의미하는 특수 값)을 참조함으로써 발생하는 오류를 **컴파일 타임(코드 작성 시점)**에 미리 방지하도록 설계된 언어적 기능 또는 프로그래밍 원칙을 말합니다.
가장 대표적인 문제가 바로 NullPointerException(NPE) 입니다. 자바의 창시자 중 한 명인 토니 호어(Tony Hoare)는 1965년 Null 참조를 발명한 것을 두고 스스로 “10억 달러짜리 실수(Billion Dollar Mistake)” 라고 표현할 정도로, null로 인한 버그는 소프트웨어 개발 역사에서 가장 흔하고 비용이 큰 오류 중 하나였습니다.
Null Safety를 지원하는 언어는 다음과 같은 질문에 컴파일러가 대신 답해줍니다.
“이 변수는 null이 될 수 있는가? 그렇다면 사용하기 전에 null 체크를 했는가?”
이 체크를 개발자가 실수로 빠뜨리면, 프로그램은 컴파일조차 되지 않습니다. 즉, 런타임에 터질 폭탄을 컴파일 타임에 미리 제거하는 것이 핵심 아이디어입니다.
2. Null Safety가 없을 때 생기는 문제
Null Safety가 없는 언어(예: Java의 초기 버전, JavaScript)에서는 아래와 같은 코드가 컴파일 자체는 문제없이 통과됩니다.
// Java (Null Safety 미지원 시절 스타일)
String name = getUserName(); // null을 반환할 수도 있음
int length = name.length(); // 런타임에 NullPointerException 발생 가능
getUserName()이 null을 반환하는 상황이 실제로 발생하기 전까지는, 이 버그는 테스트에서도, 코드 리뷰에서도 발견되지 않을 수 있습니다. 결국 운영 서버에서 장애가 나고 나서야 발견되는 경우가 많습니다.
3. Null Safety를 지원하는 언어들의 접근 방식
3-1. Kotlin: 타입 시스템에 Null 가능성을 포함
Kotlin은 타입 자체를 Nullable 타입과 Non-nullable 타입으로 구분합니다.
var name: String = "Alice" // Non-nullable: null 대입 불가
name = null // 컴파일 에러!
var nickname: String? = null // Nullable: '?'를 붙여야 null 허용
nickname = "Al" // 정상
Nullable 타입은 사용하기 전에 반드시 null 체크를 강제합니다.
val nickname: String? = getNickname()
// 방법 1: 안전 호출 연산자(?.)
val length = nickname?.length // nickname이 null이면 length도 null
// 방법 2: 엘비스 연산자(?:)로 기본값 지정
val safeLength = nickname?.length ?: 0
// 방법 3: 명시적 null 체크 (스마트 캐스트)
if (nickname != null) {
println(nickname.length) // 이 블록 안에서는 자동으로 non-null로 취급
}
3-2. Swift: Optional 타입
Swift는 값이 있을 수도, 없을 수도 있음을 Optional이라는 타입으로 명시적으로 표현합니다.
var name: String = "Alice" // Non-optional
var nickname: String? = nil // Optional
// 옵셔널 바인딩(if let)으로 안전하게 값 꺼내기
if let unwrapped = nickname {
print("닉네임: \(unwrapped)")
} else {
print("닉네임 없음")
}
// guard let으로 조기 반환 패턴
func greet(_ nickname: String?) {
guard let name = nickname else {
print("이름이 없습니다")
return
}
print("안녕, \(name)!")
}
// nil 병합 연산자(??)
let displayName = nickname ?? "익명"
3-3. TypeScript: strictNullChecks
TypeScript는 tsconfig.json에서 strictNullChecks 옵션을 켜면 Null Safety를 활성화할 수 있습니다.
function getLength(name: string | null): number {
if (name === null) {
return 0;
}
return name.length; // 이 시점에서 name은 string으로 좁혀짐(narrowing)
}
// 옵셔널 체이닝과 널 병합 연산자
const user = { profile: { nickname: null } };
const nickname = user.profile?.nickname ?? "익명";
3-4. Java: Optional 클래스 (언어 차원의 강제는 아님)
Java는 언어 자체에 Null Safety가 내장되어 있지 않지만, Java 8부터 Optional<T>을 통해 “값이 없을 수 있음"을 명시적으로 드러내는 관례를 제공합니다.
Optional<String> nickname = Optional.ofNullable(getNickname());
// 값이 있으면 처리, 없으면 기본값
String result = nickname.orElse("익명");
// 값이 있을 때만 특정 동작 수행
nickname.ifPresent(n -> System.out.println("닉네임: " + n));
// 함수형 스타일 체이닝
int length = nickname.map(String::length).orElse(0);
단, Optional은 강제성이 없어서 여전히 Optional.get()을 아무 체크 없이 호출하면 NoSuchElementException이 발생할 수 있습니다. 즉, 관례일 뿐 컴파일러가 강제하는 진짜 Null Safety는 아닙니다.
3-5. Rust: Option 열거형
Rust는 애초에 null 자체가 언어에 존재하지 않고, Option<T> 열거형으로 값의 유무를 표현합니다.
let nickname: Option<String> = None;
match nickname {
Some(n) => println!("닉네임: {}", n),
None => println!("닉네임 없음"),
}
// unwrap_or로 기본값 지정
let display_name = nickname.unwrap_or("익명".to_string());
Rust는 Option을 처리하지 않고 그냥 사용하려고 하면 컴파일 자체가 되지 않기 때문에, 가장 엄격한 형태의 Null Safety를 제공한다고 평가받습니다.
4. 언어별 비교 요약
| 언어 | Null Safety 지원 방식 | 강제 수준 |
|---|---|---|
| Kotlin | Nullable(?) / Non-nullable 타입 구분 |
컴파일러가 강제 |
| Swift | Optional 타입 | 컴파일러가 강제 |
| TypeScript | strictNullChecks 옵션 |
설정 시 컴파일러가 강제 |
| Rust | Option<T> 열거형, null 없음 |
컴파일러가 강제 (가장 엄격) |
| Java | Optional<T> (관례) |
강제 아님, 개발자 재량 |
| JavaScript | 없음 (undefined/null 혼재) |
강제 없음 |
5. Null Safety를 도입하면 좋은 점
- 런타임 오류 감소: NPE 같은 오류를 배포 전, 컴파일 시점에 잡아낼 수 있습니다.
- 코드의 의도가 명확해짐: 어떤 값이 “없을 수도 있다"는 사실이 타입에 드러나므로, 코드를 읽는 사람이 별도 문서 없이도 null 가능성을 파악할 수 있습니다.
- 방어적 코드 감소: 불필요하게 모든 곳에
if (x != null)을 남발하지 않고, 진짜 null이 가능한 지점에서만 체크하면 됩니다. - 리팩토링 안정성: 함수 시그니처가 바뀌어 null 가능성이 추가/제거되면, 컴파일러가 관련된 모든 호출부의 수정 필요 지점을 알려줍니다.
6. 마무리
Null Safety는 단순한 문법적 편의 기능이 아니라, “값이 없을 수 있다"는 사실을 타입 시스템 차원에서 다루도록 강제함으로써 소프트웨어의 신뢰성을 높이는 설계 철학입니다. Kotlin, Swift, Rust처럼 처음부터 Null Safety를 언어 차원에서 강제하는 언어들이 늘어나고 있는 것도, 그만큼 null로 인한 버그가 실무에서 얼마나 큰 비용을 초래했는지를 보여주는 방증이라 할 수 있습니다.
새 프로젝트를 시작한다면, 가능하면 Null Safety를 기본으로 지원하거나 강제할 수 있는 언어/설정을 선택하는 것이 장기적으로 유지보수 비용을 크게 줄여줄 수 있습니다.