"값이 없다"는 것은 어떤 의미일까요. 숫자 0은 없는 게 아닙니다. 분명히 0이라는 값이 있습니다. 빈 문자열 ''도 마찬가지입니다. 비어 있지만, 분명히 존재합니다. 그렇다면 진짜로 "아무것도 없음"을 표현하려면 어떻게 해야 할까요.
프로그래밍에서 이 "아무것도 없음"을 표현하는 것이 바로 null입니다. 그리고 Dart는 이 null을 다루는 방식에 특별히 신경을 쏟았습니다.
null이란 무엇인가
null은 값이 존재하지 않음을 나타내는 특별한 상태입니다.
void main() {
String? name = null; // 아직 이름이 없음
print(name); // null
}
null이 왜 필요할까요. 실제 프로그램을 생각해봅니다. 사용자가 프로필을 만들 때 전화번호를 입력하지 않을 수도 있습니다. 이때 전화번호 필드에 어떤 값을 넣어야 할까요. 0이나 ''을 넣으면 "번호가 0"이거나 "번호가 빈 문자열"이라는 의미가 되어버립니다. "번호 자체가 없음"을 표현하려면 null이 필요합니다.
void main() {
String? phoneNumber = null; // 전화번호 미입력
int? age = null; // 나이 미입력
print(phoneNumber); // null
print(age); // null
}
null이 왜 위험한가
null은 편리하지만, 잘못 다루면 프로그램이 갑자기 멈춥니다. 이것을 Null Pointer Exception(NPE) 또는 Null Reference Error라고 합니다. 유명한 컴퓨터 과학자 토니 호어는 null을 발명한 것이 "10억 달러짜리 실수"라고 말하기도 했습니다.
다음은 null을 잘못 다루면 생기는 전형적인 문제입니다.
// 만약 Dart에 Null Safety가 없다면 이런 코드가 가능했을 것입니다
void printLength(String text) {
print(text.length); // text가 null이면 런타임에 오류 발생!
}
void main() {
printLength('Hello'); // 5 — 정상
// printLength(null); // 런타임 오류! 프로그램 강제 종료
}
문제는 이 오류가 코드를 작성할 때 보이지 않는다는 점입니다. 프로그램을 실제로 실행하고, 특정 상황에서만 터집니다. 사용자가 쓰다가 앱이 튕기는 최악의 상황이 생길 수 있습니다.
Dart의 해법 — Null Safety
Dart 2.12부터 Null Safety가 기본으로 적용되었습니다. 핵심 아이디어는 간단합니다.
"null이 될 수 있는 변수와 null이 될 수 없는 변수를 처음부터 구분한다."
Dart에서 일반 타입은 기본적으로 null이 될 수 없습니다.
void main() {
String name = 'Alice';
// name = null; // 컴파일 오류! String은 null 불가
int age = 25;
// age = null; // 컴파일 오류! int는 null 불가
}
null을 담으려면 타입 뒤에 ?를 붙여서 nullable 타입으로 선언해야 합니다.
void main() {
String? name = null; // OK — String?은 null 가능
int? age = null; // OK — int?는 null 가능
name = 'Bob'; // 값을 넣는 것도 OK
age = 30; // 값을 넣는 것도 OK
print(name); // Bob
print(age); // 30
}
nullable 변수 사용하기
null이 될 수 있는 변수는 사용할 때 조심해야 합니다. null인 상태에서 메서드를 부르면 오류가 나기 때문입니다. Dart는 이것을 컴파일 시점에 잡아줍니다.
void main() {
String? name = null;
// print(name.length); // 컴파일 오류! name이 null일 수 있음
}
null인지 확인한 후에 사용하면 됩니다.
void main() {
String? name = null;
// null 확인 후 사용
if (name != null) {
print(name.length); // 이 안에서는 name이 null이 아님이 보장됨
}
name = 'Charlie';
if (name != null) {
print(name.length); // 7
}
}
null을 다루는 편리한 문법
Dart는 null을 다루기 위한 편리한 연산자를 제공합니다.
?. — null-aware 접근 연산자
변수가 null이면 null을 반환하고, null이 아니면 메서드/속성을 호출합니다.
void main() {
String? name = null;
print(name?.length); // null — 오류 없이 null 반환
name = 'Dart';
print(name?.length); // 4
}
?? — null 병합 연산자
왼쪽이 null이면 오른쪽 값을 씁니다.
void main() {
String? nickname = null;
String displayName = nickname ?? '이름 없음';
print(displayName); // 이름 없음
nickname = '다트러버';
displayName = nickname ?? '이름 없음';
print(displayName); // 다트러버
}
! — null 강제 해제 연산자
null이 아님을 개발자가 직접 보장할 때 씁니다. null인 상태에서 !를 쓰면 런타임 오류가 납니다.
void main() {
String? name = 'Alice';
// null이 아님을 확신할 때만 사용
print(name!.length); // 5
// 주의: null인 경우 오류 발생
String? empty = null;
// print(empty!.length); // 런타임 오류!
}
!는 마지막 수단입니다. 가능하면 ?.나 ??를 쓰는 것이 좋습니다.
Non-nullable 타입의 장점
Null Safety 덕분에 컴파일러가 null 관련 오류를 미리 잡아줍니다.
void printLength(String text) {
// text는 null이 될 수 없으므로 안전하게 .length 호출 가능
print(text.length);
}
void printLengthNullable(String? text) {
// text가 null일 수 있으므로 확인 필요
if (text != null) {
print(text.length);
} else {
print('텍스트가 없습니다');
}
}
void main() {
printLength('Hello'); // 5
printLengthNullable(null); // 텍스트가 없습니다
printLengthNullable('World'); // 5
}
실용적인 예제
사용자 프로필을 다루는 간단한 예제입니다.
void main() {
// 필수 정보: null 불가
String userName = '이다트';
int userId = 1001;
// 선택 정보: null 가능
String? email = null;
String? phoneNumber = null;
int? age = null;
// 정보 입력
email = '[email protected]';
age = 22;
// phoneNumber는 여전히 null
// 출력
print('=== 사용자 프로필 ===');
print('이름: $userName (ID: $userId)');
print('이메일: ${email ?? "미입력"}');
print('전화: ${phoneNumber ?? "미입력"}');
print('나이: ${age != null ? "$age세" : "미입력"}');
// null-aware 연산자 활용
int displayAge = age ?? 0;
print('나이 (기본값 0): $displayAge');
String emailDomain = email?.split('@').last ?? 'unknown';
print('이메일 도메인: $emailDomain'); // example.com
}
Null Safety 정리
| 타입 | null 가능 | 예시 |
|---|---|---|
String |
불가 | String name = 'Alice' |
String? |
가능 | String? name = null |
int |
불가 | int age = 25 |
int? |
가능 | int? age = null |
Null Safety는 처음에 낯설게 느껴질 수 있습니다. 하지만 익숙해지면 "이 변수는 절대 null이 아니다"라는 확신을 가지고 코드를 쓸 수 있게 됩니다. 버그가 줄고, 코드가 더 안전해집니다. Null Safety의 고급 기능(late 키워드, 패턴 매칭과 null 처리 등)은 PART 06에서 더 자세히 다룹니다.
다음 챕터에서는 연산자를 완전히 정복합니다. 산술, 비교, 논리 연산자뿐 아니라 Dart만의 독특한 캐스케이드 연산자와 타입 테스트 연산자까지 배웁니다.