널 안전성이란?
널 안정성이란 널 포인트 예외를 프로그램을 실행하기 전 코드를 작성하는 시점에 점검하는 것을 의미합니다. 널 포인트 예외는 객체가 특정 값이 아닌 null을 가리켜서 발생하는 오류이며 컴파일러가 걸러내지 못하고 프로그램 실행 중에 발생하므로 치명적일 수 있습니다.
널 안전성을 지원하지 않는 프로그래밍 언어들은 객체에 널값을 대입할 수 있으며 널인 객체에 접근하면 NPE가 발생하지만, 널 안전성을 지원하는 언어들은 객체가 널일 때 발생할 수 있는 오류를 코드 작성 시점에 점검해 줍니다. 즉,NPE 발생 가능성을 컴파일러가 미리 점검 해 주므로 널에 안전한 코드를 작성할 수 있습니다.
플러터 패키지 널 안전성 확인하기
플러터 2.0이 나오면서 다트 언어가 2.12.0 버전으로 업데이트되었고 이 버전부터 널 안전성을 지원하기 시작했습니다. 따라서 다트 2.12.0 버전 이상을 적용해 개발할 때는 널 안전성을 고려해서 코드를 작성해야 합니다.
플러터 프로젝트는 대부분 패키지를 이용해 개발하는데, 아직은 플러터의 모든 패키지가 널 안전성을 지원하지는 않습니다. 만약 2.12.0 버전 이상의 다트를 이용해 플러터 프로젝트를 진행한다면 널 안전성을 지원하지 않는 패키지는 이용할 수 없습니다. 따라서 사용할 패키지가 널 안전성을 지원하는지를 먼저 확인해야 합니다.
패키지가 널 안전성을 지원하는지는 다트 패키지를 소개하는 pub.dev 사이트에서 확인할 수 있습니다. 예를 들어 다음 그림은 pub.dev 사이트에서 http 패키지를 찾은 결과입니다. 패키지가 널 안전성을 지원한다면 패키지 정보에 'Null safety'라고 표시돼 있습니다.
널 허용과 널 불허
널 안전성을 지원하는 프로그래밍 언어에서는 변수를 선언할 때 널 허용(Nullable)과 널 불허(NonNull)로 구분합니다. 컴파일러에 널을 대입할 수 있는지 없는지를 명확하게 알려 줘야합니다. 그러면 널 불허 변수에 널이 대입되거나 NPE를 고려하지 않고 널 허용 변수를 이용할 때 컴파일러가 알아서 오류를 알려 줍니다. 따라서 개발자는 NPE가 발생하지 않는 코드를 작성할 수 있습니다.
다트 언어에서 변수는 기본으로 널 불허로 선언됩니다. 만약 널 허용으로 선언하려면 타입 뒤에 물음표 ?를 추가해 줘야 합니다. 밑에있는 코드에서 a1 변수는 int로 선언했으며 a2 변수는 int?로 선언했습니다. 둘 다 정수 데이터를 저장하는 타입으로 선언했지만 ?에 따라서 널 안전성에 큰 차이가 있습니다. 타입 이름을 그대로 사용해 선언한 변수에는 널을 대입할 수 없으며, 타입 이름 다음에 물음표를 붙여서 선언한 변수에는 널을 대입할 수 있습니다.
int a1 = 10;
int? a2 = 10;
int a1 =10;
int a2 = 10;
testFun() {
a1 = null; // 오류
a2 = null;
}
이처럼 널 허용, 널 불허 변수 선언법은 모든 타입에 적용됩니다.
String str1 = null; // 오류
String? str2 = null;
class User{ }
User user1 = null; // 오류
User? user2 = null;
널 불허 변수의 초기화
다트에서 모든 변수는 객체입니다. 그런데 변수를 선언하면서 초깃값을 주지 않으면 자동으로 널로 초기화됩니다. 하지만 널 불허로 선언한 변수는 선언과 동시에 널이 아닌 값으로 초기화해야 합니다. 널 불허 변수를 초기화하지 않으면 오류가 발생합니다.
int a1; // 오류
int? a2;
모든 변수에 물음표를 붙여서 선언하면 편리하지 않을까?
변수를 선언할 때 항상 물음표를 붙여서 널 허용으로 하면 객체의 멤버를 이용할 때 널 안전성 연산자를 이용해야 합니다. 이와 관련한 내용은 다음 절에서 자세히 살펴봅니다. 따라서 널 불허로 선언할 수 있는 변수를 굳이 널 허용으로 선언하면 오히려 코드가 복잡해질 수 있습니다.
그리고 널 안전성이라는 개념 자체가 변수에 널을 대입할 수 있는지를 명확하게 구분해서 사용하고 컴파일러의 도움을 받아 NPE가 발생하지 않는 코드를 작성하자는 목적입니다. 따라서 널 안전성을 지원하는 대부분 언어에서는 모든 변수를 널 불허로 선언하고 널을 대입할 가능성이 있는 변수만 선별해서 널 허용으로 선언하라고 안내합니다.
다만 널 불허 변수를 선언과 동시에 초기화해야 한다는 규칙은 톱 레벨에 선언된 변수와 클래스의 멤버 변수에만 해당합니다. 즉, 함수에서 지역 변수를 널 불허로 선언할 때는 초기화하지 않아도 됩니다.
int a1; // 오류
class User {
int a1; // 오류 널 불허 변수를 초기화 하지않아서 오류 발생
}
testFun() {
int a1; // 성공
a1 = null; // 오류 널 불허 변수에 널을 대입해서 오류 발생
}
이처럼 지역 변수는 선언과 동시에 초기화하지 않아도 되지만, 사용하기 전에는 반드시 값을 대입해 주어야 합니다. 만약 값을 대입하지 않고 사용하면 오류가 발생합니다.
// 값을 대입하지 않고 사용한 예
testFun() {
int a1;
print(a1 + 10); // 오류
}
// 값을 대입하고 사용한 예
testFun() {
int a1;
a1 = 10;
print(a1 + 10); // 성공
}
var 타입의 널 안전성
다트에서 변수를 선언할 때 타입 대신 var로 하면 대입하는 값에 따라 타입이 결정됩니다. var로 선언한 변수는 널 허용 여부도 대입하는 값에 따라 컴파일러가 자동으로 결정합니다. 따라서 var 뒤에는 물음표를 붙일 수 없습니다.
밑에 있는 코드에서 a1은 int타입으로 결정되고 a2는 널로 초기화 했으므로 dynamic 타입으로 결정됩니다. a3은 선언과 동시에 초기화하지 않았으므로 자동으로 널이 대입되고 dynamic 타입으로 결정됩니다. 그리고 a4 변수는 var 뒤에 물음표를 붙여서 컴파일 오류가 발생합니다.
// var 타입 변수에 물음표를 붙이면 오류
main() {
var a1 = 10;
var a2 = null;
var a3;
var? a4 = null; // 오류
}
다음 코드처럼 var로 선언한 변수에 값을 대입해 보면 a1 변수에 널을 대입한 부분에서만 오류가 발생합니다. a1 변수는 int 타입으로 결정되므로 널을 대입할 수 없습니다. 하지만 a2, a3 변수는 dynamic 타입으로 결정되므로 널을 포함한 모든 타입의 데이터를 대입할 수 있습니다. dynamic은 모든 타입을 의미하므로 Nullable을 포함합니다.
var a1 = 10; // int
var a2 = null; // dynamic
var a3; // dynamic
testFun() {
a1 = 20;
a1 = null; // 오류
a2 = 20;
a2 = 'hello';
a2 = null;
a3 = 20;
a3 = 'hello';
a3 = null;
}
이번에는 다른 변수를 var로 선언한 변수에 대입하는 예를 살펴보겠습니다. 다음 코드에서 var로 선언한 a1 변수에 대입한 no1의 타입이 int이므로 a1도 int 타입으로 결정됩니다. 그리고 a2 변수에 대입한 no2의 타입이 int? 이므로 a2도 int? 타입으로 결정됩니다.
// var 타입 변수에 타입이 정의된 변수 대입하기
int no1 = 10; // 널 불허
int? no2; // 널 허용
var a1 = no1; // int로 결정
var a2 = no2; // int?로 결정
testFun() {
a1 = 20;
a1 = null; // 널 불허 변수에 널을 대입해서 오류
a2 = 20;
a2 = 'hello'; // int? 타입에 문자열을 대입해서 오류
a2 = null;
}
dynamic 타입의 널 안전성
dynamic 타입에는 물음표를 추가할 수 있지만 의미가 없습니다. dynamic 타입은 모든 타입의 데이터를 대입할 수 있으므로 널을 허용하는 Nullable도 포함됩니다. 따라서 dynamic 타입으로 선언하는 것 자체가 널을 허용하는 것입니다.
// dynamic 타입의 널 허용
dynamic a1 = 10;
dynamic a2;
dynamic a3;
testFun() {
a1 = null;
a2 = null;
a3 = null;
}
널 안전성과 형 변환
널 허용(Nullable)과 널 불허(NonNull)는 타입(클래스)입니다. 따라서 형 변환과 관련된 내용도 중요합니다. 널 허용으로 선언한 변수를 널 불허로 선언한 변수에 대입할 수 있는지, 반대로 널 불허로 선언한 변수를 널 허용으로 선언한 변수에 대입할 수 있는지의 문제입니다. 결론부터 이야기하면 Nullable은 NonNull의 상위 타입입니다.
즉, int?가 int의 상위 타입입니다. 따라서 널 불허 변수를 널 호용에 대입할때는 자동으로 형 변환됩니다. 하지만 널 허용 변수를 널 불허에 대입할 때는 오류가 발생합니다.
// 자동 형 변환
int a1 = 10;
int? a2 = 10;
main() {
a1 = a2; // 오류
a2 = a1; // 성공
}
만약 널 허용 변수를 널 불허에 대입할 수 있게 하려면 다음처럼 명시적으로 형 변환해 줘야합니다. 다트에서 명시적 형 변환 연산자는 as입니다.
// 명시적 형 변환
int a1 = 10;
int? a2 = 20;
main() {
a1 = a2 as int;
print("a1: $a1, a2: #a2");
}
▶ 실행 결과
a1: 20, a2: 20
초기화를 미루는 late 연산자
앞서 알아본 대로 널 불허 변수는 선언과 동시에 초기화해야 합니다. 그런데 초기화하기 모호 할 때가 있습니다. 이때 late 연산자를 사용하면 됩니다. 그러면 변수를 널인 상태로 이용하다가 앱이 실행될 때서야 값을 결정할 수 있습니다. 즉, late는 초기화를 미루는 연산자입니다.
다음 코드에서 a1과 a2는 모두 int 타입으로 선언했으므로 널 불허입니다. 하지만 a1은 컴파일 오류가 발생하지만 a2는 late로 선언했으므로 오류가 발생하지 않습니다. 즉, 선언과 동시에 초깃값을 주지 않아도 된다는 의미입니다.
// 초기화 미루기
int a1; // 컴파일 오류
late int a2; // 성공
물론 late로 선언한 변수도 값을 대입하고 사용해야 하는 건 마찬가지입니다.
late int a2; // 성공
main() {
// print('${a2 + 10}'); // 주석을 해제하면 실행 오류
a2 = 10;
print('${a2 + 10}'); // 성공
}
▶ 실행 결과
a2: 20
'Flutter' 카테고리의 다른 글
함수 선언과 호출하기 (0) | 2024.11.12 |
---|---|
널 안전성 연산자 (0) | 2024.11.12 |
컬렉션 타입 - List, Set, Map (0) | 2024.11.11 |
var와 dynamic 타입 (0) | 2024.11.11 |
상수 변수 - const, final (0) | 2024.11.11 |