본문 바로가기
flutter

Flutter 2.0 -Null Safety

by 들풀민들레 2021. 3. 29.

 

 

책의 모든 내용을 저자 직강으로 진행한 강의는 ssamz.com 에서 들으실 수 있습니다.

 

 

 

Null Safety 란

개발되는 코드가 Null 안전성을 보장한 상태로 개발되게 하고자 하는 개념이다.

Null Safety 란 null 에 의한 NPE 를 runtime 이 아닌 edit-time 에 체크하겠다는 의미이다.

Null Safety 를 지원하지 않는 언어들은 객체에 null 대입이 기본으로 가능하며 null 상태의 객체에 멤버 접근이 이루어 지면 NPE 가 발생한다.

하지만 NPE 는 runtime 에러이며 edit-time 의 에러는 아니다. 즉 컴파일러 체크 대상이 아니라는 이야기이다.

하지만 Null Safety 를 지원하는 언어들은 객체가 null 인 상태에서 발생할수 있는 에러를 edit-time 에 발생시키겠다는 의미이다. 즉 컴파일러가 NPE 가 발생할수 있는 가능성을 미리 체크해 NPE 가 발생할수 없는 코드를 작성할 수밖에 없게 하겠다는 의미이다.

컴파일러가 미리 체크만 준다면 개발자 입장에서 컴파일 에러를 해결하면서 코드를 작성할 수밖에 없고 그렇게 되면 작성된 코드는 런타임시 NPE 는 절대 발생하지 않게 된다.

결국 컴파일러 도움을 받아 개발되는 코드가 Null 안전성을 보장한 상태로 개발되게 하고자 하는 개념이다.

Nullable 과 Non-Nullable

Null Safety 의 기본은 Nullable 과 Non-Nullable 의 구분이다.

Null Safety 가 지원이 되려면 가장 기본적으로 변수 선언시 Nullable 과 Non-Nullable 로 구분되어 선언해 주어야 한다.

이는 컴파일러에게 이 변수에 null 이 대입이 될수 있는지 될수 없는지를 명확하게 알려주자는 개념이고 이렇게 컴파일러에게 정보를 주면 컴파일러가 알아서 Non-Nullable 변수에 null 이 대입되는 상황의 에러를 발생시켜 주거나, Nullable 변수를 NPE 를 고려하지 않고 작성하는 상황을 에러를 발생시켜 주게 된다.

Dart 언어의 변수는 기본이 Non-Nullable 로 선언되는 것이며 만악 Nullable 로 선언하고자 한다면 타입명 뒤에 ? 를 추가해 주어야 한다.

 

int a1=10;
int? a2=10;

 

a1, a2 변수 모두 int 타입으로 선언되어 있다. 하지만 a1 은 int 로 선언되어 있으며 a2 는 int? 로 선언되어 있다. 모두 정수 데이터를 저장하기 위한 타입으로 선언된건 맞지만 ? 가 있고 없고의 차이는 Null Safety 에서 큰 차이가 있다.

int 로 선언되면 이 변수에는 null 대입이 불가능한 Non-Nullable 로 선언된 것이며 int? 로 선언하면 이 변수에는 null 대입이 가능한 Nullable 로 선언된 것이다.

 

int a1=10;

int? a2=10;



testFun() {

  a1=null;//error

  a2=null;

}

 

Non-Nullable 로 선언된 변수에 null 이 대입되는 것은 컴파일 에러이다. Nullable 로 선언한 변수에만 null 대입이 가능하다. 변수 선언시 타입에 ? 가 있고 없고에 따른 Non-Nullable, Nullable 관계는 int 뿐 아니라 모든 타입에 적용이 된다.

 

String str1=null;//error

String? str2=null;//ok



class User{}

User user1=null;//error

User? user2=null;//ok

 

String 클래스 타입, User 클래스 타입등 모든 변수의 타입에 ? 가 추가되지 않으면 Non-Nullable 변수가 되며 ? 가 추가되면 Nullable 변수가 된다.

 

https://www.ssamz.com/lecture_view.php?LectureStep1=51&LectureSeq=34 

 

 

Null Safety 규칙

Null Safety 개념이 이해 되었다면 이제 몇가지 규칙을 추가 정리해 보자.

Non-Nullable 변수는 선언과 동시에 초기값을 주어야 한다.

Dart 에서 모든 변수는 객체이다. 그런데 변수를 선언하면서 초기값을 주지 않으면 null 로 자동 초기화 된다. 하지만 Non-Nullable 로 선언된 변수는 null 로 자동 초기화 된다는 것이 이치에 맞지 않다. 그럼으로 Non-Nullable 선언된 변수는 선언과 동시에 명시적으로 초기값을 주어야 한다.

 

int a1;//error

int? a2;

 

a2 변수는 Nullable 로 선언되어 있음으로 선언과 동시에 초기값이 대입되지 않아도 자동으로 null 이 대입되어 초기화 된다. 하지만 a1 는 Non-Nullable 로 선언되어 있기 때문에 null 로 자동 초기화 할수 없다. The non-nullable variable ‘a1’ must be initialized. 라는 컴파일 에러가 발생하게 된다.

 

var 타입에서의 Null Safety

 

Dart 에서 변수선언시 타입을 var 로 선언하면 타입 유추(type inferred) 이다. 즉 대입되는 값에 의해 타입이 결정되는 것이다. var 로 타입 선언시 Nullable 과 Non-Nullable도 자동 유추가 된다. 그럼으로 var 타입 뒤에는 ? 를 추가할수 없다.

 

var a1=10;

var a2=null;

var a3;

var? a4=null;//error

 

위의 코드에서 a1 은 Non-Nullable 의 int 타입으로 선언된 변수로 유추가 된다. a2 는 null 로 초기화 되었음으로 dynamic 유추가 된다. 또한 a3 는 선언과 동시에 초기값을 주지 않았음으로 자동 null 이 대입되고 이렇게 되면 dynamic type 으로 유추가 된다. a4 변수의 선언 구문은 컴파일 에러이다. var 타입에 ? 추가는 불가능 하다.

 

var a1=10;//int

var a2=null;//dynamic

var a3;//dynamic

// var? a4=null;//error



testFun() {

  a1=20;

  a1=null;//error

  

  a2=20;

  a2="hello";

  a2=null;

  

  a3=20;

  a3="hello";

  a3=null;

}

 

var 로 선언된 변수에 값을 대입해 보면 a1 변수에 null 대입 부분만 에러가 발생하게 된다. a1 변수는 int 타입으로 유추가 되어 Non-Nullable 변수임으로 null 대입이 불가능하다. 하지만 a2, a3 변수는 dynamic 타입으로 유추가 됨으로 null 을 포함한 모든 타입의 데이터가 대입될수 있다.

이번에는 다른 변수 값을 var 로 선언된 변수에 대입하는 경우의 타입 유추에 대해 살펴 보자.

 

int no1=10;//Non-Nullable

int? no2;//Nullable



var a1=no1;//inferred int

var a2=no2;//inferred int?



testFun() {

  a1=20;

  a1=null;//error



  a2=20;

  a2="hello";//error

  a2=null;

}

 

위의 코드는 어찌보면 당연한 결과인데 var 로 선언된 a1 변수에 대입된 no1 의 타입이 int 임으로 a1 도 int 타입으로 유추되어 Non-Nullable 변수가 된다. a2 변수에 대입된 no2 의 타입이 int? 임으로 a2 도 int? 타입으로 유추되어 Nullable 변수가 된다.

 

dynamic 타입에서의 Null Safety

 

dynamic 타입에는 ? 가 추가되어 선언될수 있지만 의미가 없다. dynamic 타입이 대입되는 값을 한정하지 않겠다는 의미이며 모든 타입의 데이터가 대입될수 있다는 의미이다. 이 모든 타입에는 Nullable 도 포함이 된다. 그럼으로 dynamic 타입으로 선언되는 것 자체가 Nullable 로 선언되는 것이다.

 

dynamic a1 = 10;

dynamic a2;

dynamic? a3;



testFun() {

  a1=null;

  a2=null;

  a3=null;

}

 

Local Variable 에서만 Non-Nullable 변수가 선언과 동시에 초기화 되지 않아도 된다.

 

? 가 추가되지 않아 Non-Nullable 로 선언된 변수는 null 대입이 불가함으로 선언과 동시에 초기값을 주어야 한다. 하지만 이는 Top-Level 에 선언된 변수와 클래스 멤버 변수의 규칙이며 함수내에 선언되는 Local Variable 은 변수가 Non-Nullable 로 선언되었다고 하더라도 선언과 동시에 초기값을 주지 않아도 된다.

 

int a1;//error



class User {

  int a1;//error

}



testFun() {

  int a1;

  a1=null;//error

}

 

3개의 a1 변수 모두가 Non-Nullable 로 선언되어 있다. Top Level 에 선언된 변수와 클래스 멤버로 선언된 변수는 초기값을 대입하지 않아서 에러가 발생한다. 하지만 Local Variable 로 선언한 변수만 선언과 동시에 초기값을 주지 않았음에도 에러가 발생하지 않는다. 물론 Non-Nullable 로 선언되었기 때문에 null 이 대입될수는 없다.

Local Variable 로 선언된 변수는 선언과 동시에 초기값을 주지 않아도 되지만 사용하려면 꼭 초기화 해주어야 한다. 즉 값을 대입하고 이용해야 한다.

 

testFun() {

  int a1;



  print(a1+10);//error

}

 

위의 코드에서 a1 변수 선언부분에서는 에러가 발생하지 않지만 a1 변수를 이용하는 곳(더하기 연산에 이용)곳에서 컴파일 에러가 발생하게 된다. 초기값이 대입되어 있지 않기 때문에 사용할수 없다는 에러이다.

 

testFun() {

  int a1;

  a1=10;

  print(a1+10);//ok

}

 

위의 코드처럼 Local Variable 은 선언과 동시에 초기값을 주지 않아도 되지만 이용하기 전에 값을 대입하고 사용해 주어야 한다.

 

casting Test

 

Nullable 과 Non-Nullable 은 타입이다. 그럼으로 타입의 형변형과 관련된 내용도 중요하다. Nullable 로 선언된 변수가 Non-Nullable 로 선언된 변수에 대입이 가능한지, 반대로 Non-Nullable 로 선언된 변수가 Nullable 로 선언된 변수에 대입이 가능한지의 문제이다. 결론부터 이야기 하면 타입적으로 Nullable 변수는 Non-Nullable 변수의 상위 타입이다. 즉 int? 타입이 int 타입의 상위 타입이 된다. 그럼으로 Non-Nullable 변수가 Nullable 에 대입되는 것은 스마트 캐스팅에 의해 자동으로 형변형이 된다. 하지만 Nullable 변수가 Non-Nullable 에 대입되는 것은 에러이며 하고자 한다면 명시적 캐스팅을 사용해 주어야 한다.

 

int a1=10;

int? a2=10;



main() {

  a1=a2;//error

  a2=a1;//ok

}

 

a1 은 Non-Nullable 로 선언되어 있으며 a2 는 Nullable 로 선언되어 있다. a1 이 a2 에 대입되는 데는 문제가 없지만 a2 가 a1 에 대입되면 에러가 발생하게 되며 가능하게 하려면 아래의 코드처럼 명시적 캐스팅을 해주어야 한다.

 

int a1=10;

int? a2=20;



main() {

  a1=a2 as int;//ok

  print("a1: $a1, a2: $a2");//a1: 20, a2: 20

}

 

Dart 에서 명시적 캐스팅 연산자는 as 이다.

 

https://www.ssamz.com/lecture_view.php?LectureStep1=51&LectureSeq=34 

 

 

Null Safety Operator

 

! : null check operator — runtime error throw

 

어떤 변수 뒤에 ! 을 추가하면 이 변수 값이 null 인 경우 runtime error 발생하게 된다.

 

int? a2=20;
 
 main() {
   a2!;
   a2=null;
   a2!;
 }

 

위의 코드를 실행시켜 보면 마지막 라인에서 런타임 에러가 발생하게 된다.

! operator 은 nullable variable 을 non-nullable 에 대입할때도 사용이 가능하다.

 

int a1=10;
 int? a2=20;
 
 main() {
   // a1=a2;//compile error
   a1 = a2 as int;//ok
 
   a1=a2!;
   print('step 1 : a1 = $a1');//step 1 : a1 = 20
 
   a2=null;
   a1=a2!;//runtime error
   print('step 2 : a1 = $a1');
 }

 

nullable variable 을 non-nullable variable 에 대입하기 위해서는 as 연산자를 이용해 non-nullable 타입으로 캐스팅해 대입하면 된다. 그런데 nullable 변수뒤에 ! 을 추가해 대입도 가능하다. a2! 에서 런타임 에러가 발생하지 않는다는 것은 a2 가 null 이 아님이 보장되는 것임으로 non-nullable 변수에 대입이 가능해 진다.

! operator 는 변수 이외에 구문에 모두 추가가 가능하다.

 

int? some(arg){
   if(arg==10){
     return 0;
   }else {
     return null;
   }
 }
 
 main() {
   int a = some(10)!;
   print('a : $a');//a : 0
   int b = some(20)!;//runtime error
   print('b : $b');
 }

 

?. ?[ ] ?.. — null aware operator

 

객체의 값이 null 이 아닌경우 멤버에 접근하며 null 이면 멤버 접근 없이 null 을 값을 발생시키는 연산자들이다.

 

main() {
   int? no1=10;
   bool? result1 = no1?.isEven;
   print('result 1 : $result1');//true
 
   no1=null;
   bool? result2 = no1?.isEven;
   print('result 2 : $result2');//null
 }

 

nullable 로 선언된 객체들은 객체의 멤버 접근시 ?. 을 이용해 null safety 하게 코드가 작성되게 강제된다.

 

String? str="hello";
 
 main() {
   str.isEmpty;//error..... 
 }

 

위의 코드의 에러는 str 이 nullable 로 선언되었음에도 불구하고 str 의 멤버 접근시 ?. 을 사용하지 않아서 발생한 에러이다. 에러메시지는 The property ‘isEmpty’ can’t be unconditionally accessed because the receiver can be ‘null’. Try making the access conditional (using ‘?.’) or adding a null check to the target (‘!’). 이다.

?[ ] 은 List 의 데이터를 index 값으로 접근할 때 사용된다. List 객체가 null 이 아니면 index 접근이 되며 null 이면 null 을 반환하는 구문이다.

 

main() {
   List<int>? list=[10, 20, 30];
   print('list[0] : ${list?[0]}');//10
   list=null;
   print('list[0] : ${list?[0]}');//null
 }

 

late keyword

 

non-nullable 로 변수가 선언되면 선언과 동시에 초기값을 주어야 한다. 그런데 어떤 경우에는 변수가 non-nullable 로 선언되기는 하지만 선언과 동시에 초기값을 줄수 없는 경우도 있다. 즉 변수가 null 인 상태로 이용되지는 않지만 초기값이 앱이 실행되면서 결정되는 경우이다. 이때 late keyword 를 사용하면 된다. late 는 단어 뜻 그대로 초기화 시점을 뒤로 미루겠다는 의미이다.

 

int a1;//compile error
late int a2;//ok

 

a1, a2 모두 int 타입으로 선언되었음으로 non-nullable 로 선언된 것이다. 하지만 a1 은 컴파일 에러가 발생하지만 a2 는 late keyword 로 선언되었음으로 컴파일 에러가 발생하지 않는다. 선언과 동시에 초기값을 주지 않아도 된다는 의미이다. 물론 late 로 선언된 변수는 이용하기 위해서는 초기값을 주고 이용해야 한다.

 

late int a2;//ok
 
 main() {
   // print('${a2 + 10}');//runtime error
   a2=10;
   print('${a2 + 10}');//ok
 }