Flutter의 null safety 이해하기

Namkyu Park
Flutter Seoul
Published in
11 min readJul 9, 2021
Flutter 2 버전 부터 null safety를 적용한 애플리케니션 빌드 시, 위 사진과 같이 💪Building with sound null safety💪 라는 멘트가 나온다.

💪 들어가며

Flutter web stable 버전 출시와 더불어 Flutter 2.0의 가장 큰 변화는 Flutter 프로젝트에 null safety를 적용한 것이라 생각합니다. null safety가 무엇이고 어떻게 적용하는지 알아보겠습니다.

👋 null safety란

null safety는 말 그대로 null 에게서 안전한 프로그램 코드를 작성하는 것을 의미합니다. 여기서 주의 해야 할 점은 null safety 라는 용어가 null 을 없애자는 것이 아니라는 것입니다(null은 훌륭한 데이터 자료형 중 하나 입니다). 우리가 주목해야 하는 문제는, null 자체가 아니라 예상치 못한 null 을 대응하지 못하는 함수입니다.

프로그램을 개발하다 보면 런타임, 즉 애플리케이션 실행 중 null 참조 에러가 많이 발생합니다. null safety는 이 문제를 코드가 실행되기 전 컴파일러가 해당 버그를 잡아줌으로써 예상치 못한 상황을 대비할 수 있게 해줍니다. 이러한 타입 체크는 즉각적으로 에러 여부를 알 수 있어 빠르게 에러에 대응할 수 있도록 합니다. 코틀린, 스위프트와 같이 이젠 Flutter의 근간이 되는 Dart에서도 null safety 를 지원합니다. null safety를 왜 사용해야 하는지 조금 더 자세히 알고 싶으신 분들은 밑의 영상을 확인해주시기 바랍니다.

🙌 null safety 101

  • 문제 상황 인식
// null-safety 적용 이전
bool isEmpty(String string) => string.length == 0;
main(){
isEmpty(null);
}

null-safety이전 상황에서, 다음의 코드는 NoSuchMethodError 라는 런타임 에러를 호출합니다. 왜냐하면 dart에서 null은 Null클래스이며 String 클래스가 아니기 때문에 “length” 라는 이름의 메서드가 존재하지 않습니다. 이 코드는 컴파일 상 전혀 문제가 없기 때문에 정상적으로 실행은 됩니다. dart를 사용하는 flutter는 end user 즉 사용자들과 직접적으로 소통해야 했기 때문에 이러한 문제에 대한 개선이 필요했고 아래의 방법들로 이 문제를 해결하였습니다.

  • nullable 타입과 non-nullable 타입

null-safety 적용 이전부터 먼저 검토해보겠습니다. 모든 자료형은 Object의 상속을 받고 마찬가지로 모든 자료형은 Null 클래스의 슈퍼 클래스(부모 클래스) 입니다. 즉 dart 코드에서 int등의 자료형에 null을 대입한다면 이는 int 클래스에 Null클래스의 인스턴스를 대입한 것이고 당연히 Null클래스에서는 아무런 메서드가 정의되지 않았기 때문에 ‘문제 상황 인식’에서 보았던 런타임 에러가 생성하게 됩니다. 그래서 null-safety적용 후에는 위 그림과 같이 Null 클래스를 자료형과 분리하였습니다. 따라서 모든 자료형은 기본적으로 non-nullable, 즉 null을 허용하지 않는 자료형이 되었습니다. 하지만 앞서 말했든 null 자체는 굉장히 중요한 자료형 중 하나 입니다. 그래서 dart에서는 non-nullable과는 대비되어 null을 제공하는 nullable 자료형을 제공합니다. 자료형 뒤에 ‘?’를 붙이면 되고 상속관계는 다음과 같습니다.

non-nullable인 int 클래스는 nullable인 int? 클래스의 서브 클래스(자식 클래스)가 되었습니다. 즉 int?클래스에 int 인스턴스를 할당하는 것은 허용되지만, 반대의 경우는 허용 되지 않습니다. 다음의 예제를 확인하면서 자신의 지식을 점검 바랍니다.

// null safety 적용 이후
requireNonNullableVariable(int nonNullableVar){
print(nonNullableVar);
}
main(){
int? nullabaleVar = 3; // null을 대입해도 무방
requireNonNullableVariable(nullableVar); // 컴파일 에러!!
}
  • 변수의 초기화 시점

이전에는 int myVar; 등과 같은 초기화가 가능했습니다. null-safety이후 상황별 제약이 생겼는데 아래의 코드로 확인해보겠습니다.

// null safety 적용 이후// 초기화 하지 않으면 에러!
// 해당 변수는 프로그램 어디에서나 접근 가능하며 초기화 하지 않을 경우
// 컴파일러가 해당 변수가 사용가능하다는 것을 보증해줄 수 없기 때문
int topLevel = 0;
class SomeClass{
// 초기화 하지 않으면 에러!
// 위와 마찬가지 이유
static int staticVar = 1;
// 초기화 하지 않아도 됨(이경우 생성자 진입전 초기화 필요)
int initializingFormal;
int initializationList;
// 생성자에서는 생성자 body 진입 전 초기화를 끝내야 함
SomeClass(this.initializingFormal)
: initializationList = 0;

int someFunc(int para){
// 지역 변수의 경우는 해당 변수를 사용하기 전에만 초기화하면 된다.
int result;
result = 3;
return result;
}
}
  • 형 승격 (type promotion)

예제를 먼저 살펴보겠습니다.

// null safety 적용 여부 상관 없이
bool isEmptyList(Object object){
if(object is List){
// Object클래스는 isEmpty 함수가 없지만 작성 가능
// 해당 객체가 Object 타입에서 List타입으로 형 승격이 발생했기때문
return object.isEmpty;
} else{
return false;
}
}

형 승격을 간단히 말하면 연산 과정에서 객체의 타입이 변하는 것을 의미합니다. 위 예제의 경우, 컴파일러는 object is List 라는 조건을 통해 해당 객체가 List타입임을 인지하고 이 객체를 형 승격 시켰습니다. 따라서 Object 에는 없고 List에는 있는 isEmpty()함수를 호출할 수 있게 되었습니다. 이 형승격이 null check를 위해 사용될 수도 있습니다.

// null safety 적용 이후
String simpleTypePromotion(String? nullableString){
if(nullableString == null) return "this variable is null";
return nullableString; // 가능!
}

다음 메서드는 정상적으로 작동합니다. if문에서는 해당 변수가 null인지 체크 합니다. 이 if문을 넘어가면 컴파일러는 해당 변수가 null을 가질 수 없음을 파악하고 해당 변수의 타입을 String? 에서 String으로 형 승격을 시킵니다. 따라서 함수의 리턴타입이 String임에도 String? 타입의 변수를 리턴하는 것이 가능해집니다.

지금까지는 주로 non-nullable 에 관하여 살펴보았습니다. 지금부터는 nullable과 관련된 이야기를 살펴보겠습니다.

  • null 조건 연산자 (null aware operator) -> ?.
// null safety 적용 이후
String? notString = null;
print(notString?.length); // null 출력

위 예제에서 알 수 있듯, null 조건 연산자는 만약 연산자 앞의 변수가 null이라면 연산자 뒤의 작업을 시행하지 않고 null을 반환하는 연산자입니다. null이 아닌 경우는 연산자 뒤의 작업이 정상적으로 실행됩니다.

  • null값 보증 연산자 (null assertion operator) => !

해당 연산자는 nullable 변수가 null이 아닌 값을 가지고 있음을 단언할 때 사용하는 연산자입니다. 이 연산자는 코드의 흐름상 null이 아님을 확신할 수 있을 때만 사용해야 합니다. 잘못 사용하면 런타임 에러가 발생하게 됩니다.

class RecommendSystem{
final bool isRecommend;
final String? reasonOfRecommend;

RecommendSystem.yes()
: isRecommend = true,
reasonOfRecommend = "convenience";
RecommendSystem.no()
: isRecommend = false,
reasonOfRecommend = null;

@override
String toString(){
if(isRecommend == false) return "I don't recommend this system";
return "I recoomend this system because of "
+ reasonOfRecommend!.toUpperCase(); // 사용 가능!
}
}

다음 예제의 toString메서드를 보겠습니다. 클래스 생성시 위의 두 생성자만을 이용한다고 하면, 만약 첫번째 조건문에서 isRecommend가 true라면 reasonOfRecommend는 null이 아니게 되기 때문에 !. 라는 null 값 보증 연산자를 이용해 해당 변수에 null이 없음을 보증할 수 있게 됩니다.

  • late 연산자

late 연산자는 변수의 초기화를 지연시켜줍니다. 다음의 예제를 보겠습니다.

// null safety 적용 이후
// 컴파일 에러 발생!
class Developer{
String _techStack;
void frontend() => _techStack = "flutter";
void backend() => _techStack = "Spring";
void devOps() => _techStack = "aws";
String introduce() => "I'm using "+_techStack;
}

다음 코드는 컴파일 에러가 발생합니다. 생성자를 사용하지 않는 한 non-nullable인 인스턴스 변수는 생성시 초기화 하여야 합니다. 간편한 해결책으로 _techStack이라는 변수를 String? 타입으로 지정하면 되지 않냐는 생각을 할 수 있습니다. 하지만 null을 활용하지 않는 코드는 nullable변수를 쓰지 않는게 잠재적인 버그를 발생 시키지 않는다는 점에서 좋습니다. 이때 우리는 late연산자를 사용합니다.

// null safety 적용 이후
class Developer{
late String _techStack;
void frontend() => _techStack = "flutter";
void backend() => _techStack = "Spring";
void devOps() => _techStack = "aws";
String introduce() => "I'm using "+_techStack;
}

위의 예제에서처럼 late연산자를 사용한 변수는 해당 변수가 사용되기 전에만 변수를 초기화 하면 됩니다. 중요한 점은 late연산자는 초기화를 늦추어주지만 null을 허용하지 않는다는 점입니다. late 연산자 역시 코드 작성 시 초기화 전 해당 변수를 사용한다면 에러가 발생합니다.

  • named parameter

null safety 적용 이전에는 named parameter에 아무 값을 집어넣지 않는 경우 컴파일러에서 null을 자동주입해서 아무 이상 없었지만, null safety 적용 이후 non-nullable 타입을 named parameter로 사용한 경우를 생각해봅시다. 이때 해당 파라미터에 기본값을 적용 시키지 않으면 반드시 required 키워드를 사용하여 해당 파라미터에 값을 필수적으로 넘겨주어야 한다는 것을 명시해야 합니다. 밑의 예제를 확인하면서 이해하시면 됩니다.

// null safety 적용 이전
void someFunc(int a,{int b}){} //가능
void someFunc(int a,{int b}){} //가능
// null safety 적용 이후
void someFunc(int? a,{required int b}){} //가능
// void someFunc(int? a,{int b}){} 불가!

👊 마치며

많은 내용을 한편의 글로 담다 보니 생략된 내용이 많습니다. 시간이 되시는 분들이 이 링크에 있는 아티클을 꼭 읽어주시기 바랍니다. 문의 사항은 lak9348@gmail.com으로 이메일 주시면 됩니다. 감사합니다.

🤝 참고

2021 Google I/O Dart null safety in Action

2021 Google I/O Why null safety?

Understanding null safety

--

--