PART 02에서 Null Safety의 기본을 배웠습니다. ?를 붙이면 null이 가능하고, 붙이지 않으면 null이 불가능하다는 것을요. 그런데 Dart의 공식 문서를 보면 "Sound Null Safety"라는 표현이 등장합니다. 왜 그냥 "Null Safety"가 아니라 "Sound"라는 수식어가 붙었을까요. 이 챕터에서는 그 "Sound"의 의미를 파고듭니다.
"Sound"란 무엇인가
"Sound"는 음악의 소리가 아닙니다. 프로그래밍 언어 이론에서 "sound"는 이 언어가 보장한 것은 런타임에서도 반드시 참이다라는 의미입니다. 쉽게 말하면 거짓말하지 않는다는 뜻입니다.
Dart가 "이 변수는 null이 아니다"라고 컴파일 타임에 확인했다면, 런타임에서도 그 변수는 절대 null이 아닙니다. 예외 없이, 100% 보장됩니다. 이것이 Sound Null Safety의 핵심입니다.
void greet(String name) {
// Dart는 여기서 name이 절대 null이 아님을 보장합니다.
// 컴파일러가 확인했고, 런타임도 동의합니다.
print('안녕하세요, $name!');
}
void main() {
greet('다트'); // 안녕하세요, 다트!
// greet(null); // 컴파일 오류 — 애초에 불가능합니다
}
name이 String 타입이라면, 그것은 컴파일 타임의 약속이면서 동시에 런타임의 현실입니다. 둘이 일치합니다. 이것이 "sound"입니다.
TypeScript와 비교해보기
TypeScript도 null 안전성을 위한 strictNullChecks 옵션이 있습니다. 하지만 TypeScript는 "unsound"합니다. 이게 무슨 뜻인지 살펴봅니다.
TypeScript에서는 타입 단언(type assertion)이나 as any를 통해 컴파일러를 속일 수 있습니다.
// TypeScript 예시 (Dart 코드가 아닙니다)function greet(name: string) { console.log(name.toUpperCase());}// 컴파일러를 속이는 방법const value: any = null;greet(value as string); // 컴파일은 통과하지만, 런타임 오류 발생!
TypeScript는 컴파일 타임에 "이 값은 string이야"라고 했지만, 런타임에서 그 약속이 깨집니다. 컴파일러와 런타임이 서로 다른 말을 하는 것입니다.
Dart는 다릅니다. Dart에서 String 타입 변수에 null을 넣는 것은 어떤 우회로를 써도 허용되지 않습니다.
void greet(String name) {
print(name.toUpperCase());
}
void main() {
// Dart에는 TypeScript의 'as any' 같은 탈출구가 없습니다.
// ! 연산자도 non-nullable 타입을 null로 만들지 않습니다.
// dynamic을 써도 런타임 타입 검사가 남아있습니다.
dynamic value = null;
greet(value); // 런타임 오류는 나지만, 타입 시스템이 무너지지 않습니다
}
Dart의 타입 시스템은 구멍이 없습니다. 컴파일 타임의 보장이 런타임까지 이어집니다.
Kotlin과 Swift의 Optional과 비교
Kotlin과 Swift도 유사한 null 안전성을 제공합니다. Dart와 문법적으로도 비슷합니다.
Kotlin에서는 String?이 nullable이고 String이 non-nullable입니다. Dart와 같습니다.
// Kotlin 예시fun greet(name: String) { // non-nullable println("Hello, $name")}fun greetNullable(name: String?) { // nullable println("Hello, ${name ?: "unknown"}")}
Swift에서는 Optional<String> 또는 String?이 nullable에 해당합니다.
// Swift 예시
func greet(name: String) { // non-nullable
print("Hello, \(name)")
}
func greetOptional(name: String?) { // Optional
print("Hello, \(name ?? "unknown")")
}
Dart와의 차이를 정리하면 다음과 같습니다.
| 언어 | nullable 표기 | Sound 여부 | 특징 |
|---|---|---|---|
| Dart | String? |
Sound | 컴파일+런타임 완전 보장 |
| Kotlin | String? |
Sound에 가까움 | 플랫폼 타입(Java 연동)에서 예외 가능 |
| Swift | String? |
Sound에 가까움 | Objective-C 브리지에서 예외 가능 |
| TypeScript | string | null |
Unsound | as any 등으로 우회 가능 |
Kotlin과 Swift는 Java, Objective-C 같은 null 안전성이 없는 언어와 연동하는 과정에서 예외가 발생할 수 있습니다. 반면 Dart는 순수하게 Dart 생태계 안에서 완전한 sound를 달성합니다.
Sound Null Safety가 실무에서 주는 이점
이론적인 "sound"가 실제 개발에서는 어떤 차이를 만들까요.
컴파일러가 더 적극적으로 도와줍니다
non-nullable 변수에는 null 체크가 필요 없다는 것을 컴파일러가 압니다. 그래서 IDE에서 불필요한 경고를 보여주지 않습니다. 반대로 nullable 변수를 null 체크 없이 쓰면 즉시 오류를 보여줍니다.
void processUser(String name, int? age) {
// name은 non-nullable — null 체크 없이 바로 사용 가능합니다
print(name.toUpperCase());
// age는 nullable — null 체크 후에만 사용 가능합니다
if (age != null) {
print('나이: $age');
}
}
런타임 성능이 향상됩니다
Sound Null Safety 덕분에 Dart 컴파일러는 런타임에 불필요한 null 체크를 생략할 수 있습니다. non-nullable 타입은 런타임에서 null 체크를 할 필요가 없다는 것을 컴파일러가 알기 때문입니다. Flutter 팀의 발표에 따르면 Null Safety 도입 후 앱 성능이 수 퍼센트 향상되었습니다.
문서화 효과가 있습니다
타입 선언만 봐도 이 변수가 null이 될 수 있는지 없는지 알 수 있습니다. 주석이 필요 없습니다.
// 아래 함수 시그니처만 봐도 의도가 명확합니다
String formatName(String firstName, String? lastName) {
// firstName은 반드시 있어야 합니다
// lastName은 없을 수도 있습니다
if (lastName != null) {
return '$firstName $lastName';
}
return firstName;
}
팀 협업에서 신뢰가 생깁니다
다른 사람이 작성한 함수를 호출할 때, 반환 타입이 String이라면 절대 null이 오지 않는다는 것을 알 수 있습니다. String?이라면 null을 처리해야 한다는 것을 알 수 있습니다. 함수 내부를 들여다보거나 문서를 뒤질 필요가 없습니다.
Sound Null Safety는 Dart 2.12부터
Dart의 Sound Null Safety는 2021년 3월, Dart 2.12 버전에서 안정화되었습니다. 현재 사용하는 Dart 3.x에서는 Sound Null Safety가 기본이며, 선택 사항이 아닙니다. Null Safety가 없는 "레거시 모드"는 Dart 3에서 완전히 제거되었습니다.
즉, 지금 Dart를 배우는 여러분은 처음부터 Sound Null Safety 환경에서 시작하는 것입니다. 오히려 좋습니다. Null Safety가 없던 시절을 경험하지 않아도 됩니다.
// Dart 3.x — Sound Null Safety가 기본입니다
void main() {
// 이 두 줄은 완전히 다른 타입입니다
String a = 'hello'; // null 불가 — 컴파일+런타임 보장
String? b = null; // null 가능 — 사용 시 확인 필요
print(a.length); // 안전합니다, 바로 호출 가능합니다
print(b?.length); // null-aware 연산자로 안전하게 접근합니다
}
다음 챕터에서는 Dart의 타입 계층 구조를 탐험합니다. String과 String?이 왜 완전히 다른 타입인지, Never와 Null 타입은 무엇인지, 그리고 nullable 타입을 non-nullable로 변환하는 여러 방법을 배웁니다.